install: Compare parallel index freshness at whole-second resolution

The install handler runs `cmake --install -j` in parallel only when the
CMakeFiles/InstallScripts.json index is at least as new as
CMakeFiles/cmake.check_cache.  Both files are written during a single
generate step, so any ordering between them is sub-second.  On
filesystems that do not order two near-simultaneous writes by their
sub-second modification time (e.g. NFS on AIX), a fresh index can appear
older than the marker and the install spuriously falls back to serial.

Compare the two modification times at whole-second resolution.  This
ignores sub-second jitter within one generate step while still detecting
a genuinely stale index from an older CMake reconfigure, which is always
at least a configure run older.

Fixes: #27919
This commit is contained in:
Daksh Mamodiya
2026-07-08 19:40:45 +02:00
parent 40277a12b8
commit 06fe796c9a
3 changed files with 56 additions and 18 deletions
+12 -5
View File
@@ -64,13 +64,20 @@ cmInstallScriptHandler::cmInstallScriptHandler(
this->Directories.push_back(cmSystemTools::GetFilenamePath(script));
};
int compare = 1;
// Trust the parallel install index unless the cache marker is newer. Both
// files are written back-to-back while generating this build tree, so any
// ordering between them lives in a sub-second window that some filesystems
// (e.g. NFS on AIX) do not resolve by write time. Compare at whole-second
// resolution: a genuinely stale index left by an older CMake reconfigure is
// always at least a configure run (seconds) older, while the sub-second
// jitter of a single generate step is ignored.
bool indexIsFresh = false;
if (cmSystemTools::FileExists(file)) {
cmSystemTools::FileTimeCompare(
cmStrCat(this->BinaryDir, "/CMakeFiles/cmake.check_cache"), file,
&compare);
long int const cacheTime = cmSystemTools::ModifiedTime(
cmStrCat(this->BinaryDir, "/CMakeFiles/cmake.check_cache"));
indexIsFresh = cacheTime <= cmSystemTools::ModifiedTime(file);
}
if (compare < 1) {
if (indexIsFresh) {
Json::CharReaderBuilder rbuilder;
auto jsonReader =
std::unique_ptr<Json::CharReader>(rbuilder.newCharReader());
@@ -42,7 +42,7 @@ function(install_test test)
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})
run_cmake_command(${test}-install ${CMAKE_COMMAND} --install . ${ARGS_ARGS})
unset(RunCMake-check-file)
endif()
endfunction()
@@ -75,19 +75,7 @@ function(install_fail_test test fixture)
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()
@@ -100,3 +88,31 @@ install_fail_test(serial-fatal install-fail FAIL_MODE fatal)
# 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)
# The stale-index guard exercised by out-of-date-json above must be
# recoverable: once CMakeFiles/InstallScripts.json looks older than
# CMakeFiles/cmake.check_cache, a reconfigure rewrites both files in one
# generate step so the install runs in parallel again. The handler compares
# their modification times at whole-second resolution, so the freshly generated
# index is not spuriously demoted to the serial fallback on filesystems that
# commit the two configure-time writes out of order within a second.
function(reconfigure_parallel_test test)
set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/${test}-install)
set(RunCMake_TEST_OPTIONS -DINSTALL_PARALLEL=ON -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()
run_cmake(install)
set(RunCMake_TEST_NO_CLEAN 1)
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)
run_cmake_command(${test}-reconfigure ${CMAKE_COMMAND} .)
set(INSTALL_MANIFEST ${RunCMake_TEST_BINARY_DIR}/install_manifest.txt)
set(INSTALL_COUNT 5)
set(RunCMake-check-file check-manifest.cmake)
run_cmake_command(${test}-install ${CMAKE_COMMAND} --install .)
unset(RunCMake-check-file)
endfunction()
reconfigure_parallel_test(out-of-date-json-reconfigure)
@@ -0,0 +1,15 @@
\[1\/5\] .*
\-\- Installing:[^
]*
\[2\/5\] .*
\-\- Installing:[^
]*
\[3\/5\] .*
\-\- Installing:[^
]*
\[4\/5\] .*
\-\- Installing:[^
]*
\[5\/5\] .*
\-\- Installing:[^
]*