mirror of
https://github.com/Kitware/CMake.git
synced 2025-10-16 22:37:30 +08:00
FetchContent: Don't update timestamps if files don't change
The refactoring in 17e5516e60
(FetchContent: Invoke steps directly and
avoid a separate sub-build, 2021-01-29) uses a different way of writing
out the step scripts and updating time stamps when steps are executed.
That inadvertently always wrote out the scripts for custom commands,
even when the contents didn't change. This caused their timestamp to
always be updated, resulting in those steps always being seen as
out-of-date and needing to be re-executed.
The way timestamps were checked to determine whether to re-execute
a step also did not adequately account for file systems which only have
second-resolution timestamps. The IS_NEWER_THAN if condition also
returns true when timestamps are the same, so one needs to use the
negative form to get a true "is newer than" test.
ExternalProject is not susceptible to this problem because it uses
file(GENERATE) to write out the script files and that only updates the file's
timestamp if the contents change. It also mostly leaves timestamp
checking to the build tool.
This commit is contained in:
@@ -2579,11 +2579,24 @@ function(_ep_write_command_script
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(genex_supported)
|
if(genex_supported)
|
||||||
# Only written at generation phase
|
# Only written at generation phase. This will only change the file's
|
||||||
|
# timestamp if the contents change.
|
||||||
file(GENERATE OUTPUT "${script_filename}" CONTENT "${script_content}")
|
file(GENERATE OUTPUT "${script_filename}" CONTENT "${script_content}")
|
||||||
else()
|
else()
|
||||||
# Written immediately, needed if script has to be invoked in configure phase
|
# Update the file immediately, needed if script has to be invoked in the
|
||||||
file(WRITE "${script_filename}" "${script_content}")
|
# configure phase (e.g. via FetchContent). We need to be careful to avoid
|
||||||
|
# updating the timestamp if the file contents don't change. The file(WRITE)
|
||||||
|
# command always updates the file, so avoid it if we don't need to call it.
|
||||||
|
set(doWrite TRUE)
|
||||||
|
if(EXISTS "${script_filename}")
|
||||||
|
file(READ "${script_filename}" existing_content)
|
||||||
|
if(existing_content STREQUAL script_content)
|
||||||
|
set(doWrite FALSE)
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
if(doWrite)
|
||||||
|
file(WRITE "${script_filename}" "${script_content}")
|
||||||
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
endfunction()
|
endfunction()
|
||||||
@@ -3916,7 +3929,12 @@ function(_ep_do_preconfigure_steps_now name)
|
|||||||
|
|
||||||
if(NOT need_to_run)
|
if(NOT need_to_run)
|
||||||
foreach(dep_file ${script_file} ${_EPdepends_${STEP}})
|
foreach(dep_file ${script_file} ${_EPdepends_${STEP}})
|
||||||
if(NOT EXISTS ${dep_file} OR ${dep_file} IS_NEWER_THAN ${stamp_file})
|
# IS_NEWER_THAN is also true if the timestamps are the same. On some
|
||||||
|
# file systems, we only have second resolution timestamps and the
|
||||||
|
# likelihood of having the same timestamp is high. Use the negative
|
||||||
|
# form to ensure we actually get a true "is newer than" test.
|
||||||
|
if(NOT EXISTS ${dep_file} OR
|
||||||
|
NOT ${stamp_file} IS_NEWER_THAN ${dep_file})
|
||||||
set(need_to_run TRUE)
|
set(need_to_run TRUE)
|
||||||
break()
|
break()
|
||||||
endif()
|
endif()
|
||||||
|
@@ -27,6 +27,36 @@ run_cmake_with_options(ManualSourceDirectoryRelative
|
|||||||
-D "FETCHCONTENT_SOURCE_DIR_WITHPROJECT:STRING=WithProject"
|
-D "FETCHCONTENT_SOURCE_DIR_WITHPROJECT:STRING=WithProject"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
function(run_FetchContent_TimeStamps)
|
||||||
|
set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/TimeStamps)
|
||||||
|
file(REMOVE_RECURSE "${RunCMake_TEST_BINARY_DIR}")
|
||||||
|
file(MAKE_DIRECTORY "${RunCMake_TEST_BINARY_DIR}")
|
||||||
|
|
||||||
|
# First run should execute the commands
|
||||||
|
run_cmake(TimeStamps)
|
||||||
|
|
||||||
|
# Ensure that the file checks we use in the TimeStampsRerun-check.cmake script
|
||||||
|
# will not be defeated by file systems with only one second resolution.
|
||||||
|
# The IS_NEWER_THAN check returns TRUE if the timestamps of the two files are
|
||||||
|
# the same, which has been observed where filesystems only have one second
|
||||||
|
# resolution.
|
||||||
|
set(cmpTimeStamp ${RunCMake_TEST_BINARY_DIR}/cmpTimeStamp.txt)
|
||||||
|
set(checkTimeStamp ${RunCMake_TEST_BINARY_DIR}/cmpTimeStampCheck.txt)
|
||||||
|
file(TOUCH ${cmpTimeStamp})
|
||||||
|
file(TOUCH ${checkTimeStamp})
|
||||||
|
if("${cmpTimeStamp}" IS_NEWER_THAN "${checkTimeStamp}")
|
||||||
|
execute_process(
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E sleep 1.125
|
||||||
|
COMMAND_ERROR_IS_FATAL LAST
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Run again with no changes, no commands should re-execute
|
||||||
|
set(RunCMake_TEST_NO_CLEAN 1)
|
||||||
|
run_cmake(TimeStampsRerun)
|
||||||
|
endfunction()
|
||||||
|
run_FetchContent_TimeStamps()
|
||||||
|
|
||||||
function(run_FetchContent_DirOverrides)
|
function(run_FetchContent_DirOverrides)
|
||||||
set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/DirOverrides-build)
|
set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/DirOverrides-build)
|
||||||
file(REMOVE_RECURSE "${RunCMake_TEST_BINARY_DIR}")
|
file(REMOVE_RECURSE "${RunCMake_TEST_BINARY_DIR}")
|
||||||
|
2
Tests/RunCMake/FetchContent/TimeStamps-stdout.txt
Normal file
2
Tests/RunCMake/FetchContent/TimeStamps-stdout.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
.* *download executed
|
||||||
|
.* *patch executed
|
14
Tests/RunCMake/FetchContent/TimeStamps.cmake
Normal file
14
Tests/RunCMake/FetchContent/TimeStamps.cmake
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
include(FetchContent)
|
||||||
|
|
||||||
|
# Do nothing for an update because it would result in always re-running the
|
||||||
|
# patch step. We want to test that a patch step that only depends on the
|
||||||
|
# download step is not re-run unnecessarily.
|
||||||
|
FetchContent_Declare(customCommands
|
||||||
|
PREFIX ${CMAKE_CURRENT_BINARY_DIR}
|
||||||
|
DOWNLOAD_COMMAND "${CMAKE_COMMAND}" -E echo "download executed"
|
||||||
|
UPDATE_COMMAND ""
|
||||||
|
PATCH_COMMAND "${CMAKE_COMMAND}" -E echo "patch executed"
|
||||||
|
)
|
||||||
|
|
||||||
|
set(FETCHCONTENT_QUIET FALSE)
|
||||||
|
FetchContent_MakeAvailable(customCommands)
|
38
Tests/RunCMake/FetchContent/TimeStampsRerun-check.cmake
Normal file
38
Tests/RunCMake/FetchContent/TimeStampsRerun-check.cmake
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
set(cmpFile ${RunCMake_TEST_BINARY_DIR}/cmpTimeStamp.txt)
|
||||||
|
set(scriptDir ${RunCMake_TEST_BINARY_DIR}/tmp)
|
||||||
|
set(stampDir ${RunCMake_TEST_BINARY_DIR}/src/customcommands-stamp)
|
||||||
|
|
||||||
|
set(errorMessages)
|
||||||
|
if(NOT EXISTS "${cmpFile}")
|
||||||
|
list(APPEND errorMessages " ${cmpFile} is missing")
|
||||||
|
else()
|
||||||
|
foreach(script IN ITEMS mkdirs download patch)
|
||||||
|
set(scriptFile "${scriptDir}/customcommands-${script}.cmake")
|
||||||
|
if(NOT EXISTS "${scriptFile}")
|
||||||
|
list(APPEND errorMessages " ${scriptFile} is missing")
|
||||||
|
elseif(NOT "${cmpFile}" IS_NEWER_THAN "${scriptFile}")
|
||||||
|
list(APPEND errorMessages " ${scriptFile} was unexectedly updated")
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
|
||||||
|
# special case, not a script, has different extension
|
||||||
|
set(repoInfoFile "${scriptDir}/customcommands-download-repoinfo.txt")
|
||||||
|
if(NOT EXISTS "${repoInfoFile}")
|
||||||
|
list(APPEND errorMessages " ${repoInfoFile} is missing")
|
||||||
|
elseif(NOT "${cmpFile}" IS_NEWER_THAN "${repoInfoFile}")
|
||||||
|
list(APPEND errorMessages " ${repoInfoFile} was unexectedly updated")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
foreach(step IN ITEMS download patch)
|
||||||
|
set(stampFile "${stampDir}/customcommands-${step}")
|
||||||
|
if(NOT EXISTS "${stampFile}")
|
||||||
|
list(APPEND errorMessages " ${stampFile} is missing")
|
||||||
|
elseif(NOT "${cmpFile}" IS_NEWER_THAN "${stampFile}")
|
||||||
|
list(APPEND errorMessages " ${stampFile} was unexectedly updated")
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(errorMessages)
|
||||||
|
list(JOIN errorMessages "\n" RunCMake_TEST_FAILED)
|
||||||
|
endif()
|
1
Tests/RunCMake/FetchContent/TimeStampsRerun.cmake
Normal file
1
Tests/RunCMake/FetchContent/TimeStampsRerun.cmake
Normal file
@@ -0,0 +1 @@
|
|||||||
|
include(${CMAKE_CURRENT_LIST_DIR}/TimeStamps.cmake)
|
Reference in New Issue
Block a user