Files
cmake/Tests/RunCMake/InstallParallel/RunCMakeTest.cmake
T
Daksh Mamodiya ae5f506906 Install: Exit non-zero when an install script fails
`cmake --install` did not propagate per-script failures into its
process exit code.

In parallel mode, cmInstallScriptHandler::Install spawned each install
script as a child process but never inspected the child exit status or
termination signal, and always returned 0.  It now reads each child's
status after the event loop, prints the failing script's exit code or
signal, and returns non-zero if any script failed.  A failed parallel
install also no longer writes the combined install_manifest.txt, so a
partial manifest is not mistaken for a complete install.

In serial mode, GetScripts() returns the top-level cmake_install.cmake
once per component and per configuration, so multiple scripts run only
when installing several components or configurations at once.  The loop
overwrote its result on every iteration, so an earlier script that
failed via cmake_language(EXIT) was masked by a later one that
succeeded.  The loop now stops at the first failure.  This is a no-op
for a single component and configuration; it changes only installs of
multiple components or configurations, which now stop at the first
failed script and report it instead of attempting the rest.

Fixes: #27906
2026-06-30 18:06:50 +02:00

103 lines
4.4 KiB
CMake

include(RunCMake)
function(install_test test)
cmake_parse_arguments(ARGS "PARALLEL;NINJA;TOUCH_CACHE" "ARGS;COMPONENT" "" ${ARGN})
set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/${test}-install)
set(RunCMake_TEST_OPTIONS -DINSTALL_PARALLEL=${ARGS_PARALLEL} -DCMAKE_INSTALL_PREFIX=install)
set(RunCMake_TEST_OUTPUT_MERGE 1)
if (NOT RunCMake_GENERATOR_IS_MULTI_CONFIG)
list(APPEND RunCMake_TEST_OPTIONS -DCMAKE_BUILD_TYPE=Debug)
endif()
if (ARGS_COMPONENT)
list(APPEND ARGS_ARGS "--component ${ARGS_COMPONENT}")
endif()
run_cmake(install)
set(RunCMake_TEST_NO_CLEAN 1)
if (ARGS_TOUCH_CACHE)
run_cmake_command(${test}-sleep ${CMAKE_COMMAND} -E sleep 2)
run_cmake_command(${test}-touch
${CMAKE_COMMAND} -E touch ${RunCMake_TEST_BINARY_DIR}/CMakeFiles/cmake.check_cache)
endif()
if (ARGS_NINJA)
if (ARGS_PARALLEL)
set(INSTALL_COUNT 5)
else()
set(INSTALL_COUNT 1)
endif()
set(RunCMake-check-file check-num-installs.cmake)
run_cmake_command(${test}-install ${CMAKE_COMMAND} -E env --unset=NINJA_STATUS ${CMAKE_COMMAND} --build . --config Debug ${ARGS_ARGS})
unset(RunCMake-check-file)
else()
if (ARGS_COMPONENT)
if(ARGS_COMPONENT MATCHES "^[a-zA-Z0-9_.+-]+$")
set(INSTALL_MANIFEST "install_manifest_${ARGS_COMPONENT}.txt")
else()
string(MD5 COMPONENT_HASH "${ARGS_COMPONENT}")
set(INSTALL_MANIFEST "install_manifest_${COMPONENT_HASH}.txt")
endif()
set(INSTALL_COUNT 0)
else()
set(INSTALL_MANIFEST "install_manifest.txt")
set(INSTALL_COUNT 5)
endif()
set(INSTALL_MANIFEST ${RunCMake_TEST_BINARY_DIR}/${INSTALL_MANIFEST})
set(RunCMake-check-file check-manifest.cmake)
run_cmake_command(${test}-install ${CMAKE_COMMAND} -E env --unset=NINJA_STATUS ${CMAKE_COMMAND} --install . ${ARGS_ARGS})
unset(RunCMake-check-file)
endif()
endfunction()
install_test(parallel PARALLEL ARGS "-j 4")
install_test(parallel-no-space PARALLEL ARGS "-j4")
install_test(no-parallel ARGS "-j 4")
install_test(out-of-date-json TOUCH_CACHE PARALLEL ARGS "-j 4")
install_test(component PARALLEL ARGS "-j 4" COMPONENT "ALPHANUMERIC123")
install_test(component-hash PARALLEL ARGS "-j 4" COMPONENT "@#$")
if(RunCMake_GENERATOR MATCHES "Ninja")
install_test(ninja-parallel ARGS "-t install/parallel" NINJA PARALLEL)
install_test(ninja-no-parallel ARGS "-t install" NINJA)
endif()
# Exit-code fidelity: a failing install must report a non-zero exit code.
function(install_fail_test test fixture)
cmake_parse_arguments(ARG "PARALLEL" "FAIL_MODE" "INSTALL_ARGS" ${ARGN})
set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/${test}-build)
set(RunCMake_TEST_OPTIONS
-DINSTALL_PARALLEL=${ARG_PARALLEL}
-DCMAKE_INSTALL_PREFIX=install)
if (ARG_FAIL_MODE)
list(APPEND RunCMake_TEST_OPTIONS -DFAIL_MODE=${ARG_FAIL_MODE})
endif()
set(RunCMake_TEST_OUTPUT_MERGE 1)
if (NOT RunCMake_GENERATOR_IS_MULTI_CONFIG)
list(APPEND RunCMake_TEST_OPTIONS -DCMAKE_BUILD_TYPE=Debug)
endif()
run_cmake(${fixture})
set(RunCMake_TEST_NO_CLEAN 1)
if (ARG_PARALLEL)
# The install runs in parallel only when CMakeFiles/InstallScripts.json is
# at least as new as CMakeFiles/cmake.check_cache; otherwise the handler
# falls back to running the top-level cmake_install.cmake serially. Both
# files are written during the same configuration, and their relative
# modification times are not reliable on every filesystem, so make the JSON
# newest explicitly to keep this test from intermittently exercising the
# serial fallback (which omits the per-script parallel diagnostics).
file(TOUCH_NOCREATE
${RunCMake_TEST_BINARY_DIR}/CMakeFiles/InstallScripts.json)
endif()
run_cmake_command(${test}
${CMAKE_COMMAND} -E env --unset=NINJA_STATUS
${CMAKE_COMMAND} --install . ${ARG_INSTALL_ARGS})
endfunction()
# Parallel: any failing child must make the install exit non-zero.
install_fail_test(parallel-fatal install-fail PARALLEL FAIL_MODE fatal INSTALL_ARGS -j 4)
install_fail_test(parallel-exit install-fail PARALLEL FAIL_MODE exit INSTALL_ARGS -j 4)
# Serial: fail-fast is preserved on a fatal error (regression guard) ...
install_fail_test(serial-fatal install-fail FAIL_MODE fatal)
# ... and a status-only failure of an earlier component is no longer masked
# by a later success (and the later component is not installed).
install_fail_test(serial-mask install-component-mask
INSTALL_ARGS --component comp_fail --component comp_ok)