Browse Source

ExternalProject: Add DOWNLOAD_EXTRACT_TIMESTAMP option and policy

Add the option to keep the current filestamps when extracting an
archive in ExternalProject_Add.

Enabling this option makes the behavior consistent with how
ExternalProject_Add is used when checking out code from revision
control instead of an archive.

Fixes: #22746
Kasper Laudrup 3 years ago
parent
commit
a283e58b51

+ 1 - 0
Help/manual/cmake-policies.7.rst

@@ -58,6 +58,7 @@ Policies Introduced by CMake 3.24
 .. toctree::
 .. toctree::
    :maxdepth: 1
    :maxdepth: 1
 
 
+   CMP0135: ExternalProject ignores timestamps in archives by default for the URL download method. </policy/CMP0135>
    CMP0134: Fallback to \"HOST\" Windows registry view when \"TARGET\" view is not usable. </policy/CMP0134>
    CMP0134: Fallback to \"HOST\" Windows registry view when \"TARGET\" view is not usable. </policy/CMP0134>
    CMP0133: The CPack module disables SLA by default in the CPack DragNDrop Generator. </policy/CMP0133>
    CMP0133: The CPack module disables SLA by default in the CPack DragNDrop Generator. </policy/CMP0133>
    CMP0132: Do not set compiler environment variables on first run. </policy/CMP0132>
    CMP0132: Do not set compiler environment variables on first run. </policy/CMP0132>

+ 29 - 0
Help/policy/CMP0135.rst

@@ -0,0 +1,29 @@
+CMP0135
+-------
+
+.. versionadded:: 3.24
+
+When using the ``URL`` download method with the :command:`ExternalProject_Add`
+command, CMake 3.23 and below sets the timestamps of the extracted contents
+to the same as the timestamps in the archive. When the ``URL`` changes, the
+new archive is downloaded and extracted, but the timestamps of the extracted
+contents might not be newer than the previous contents. Anything that depends
+on the extracted contents might not be rebuilt, even though the contents may
+change.
+
+CMake 3.24 and above prefers to set the timestamps of all extracted contents
+to the time of the extraction. This ensures that anything that depends on the
+extracted contents will be rebuilt whenever the ``URL`` changes.
+
+The ``DOWNLOAD_EXTRACT_TIMESTAMP`` option to the
+:command:`ExternalProject_Add` command can be used to explicitly specify how
+timestamps should be handled. When ``DOWNLOAD_EXTRACT_TIMESTAMP`` is not
+given, this policy controls the default behavior. The ``OLD`` behavior for
+this policy is to restore the timestamps from the archive. The ``NEW``
+behavior sets the timestamps of extracted contents to the time of extraction.
+
+This policy was introduced in CMake version 3.24.  CMake version |release|
+warns when the policy is not set and uses ``OLD`` behavior.  Use the
+:command:`cmake_policy` command to set it to ``OLD`` or ``NEW`` explicitly.
+
+.. include:: DEPRECATED.txt

+ 8 - 0
Help/release/dev/ExternalProject-no-extract-timestamp.rst

@@ -0,0 +1,8 @@
+ExternalProject-no-extract-timestamp
+------------------------------------
+
+* The :command:`ExternalProject_Add` command gained a new
+  ``DOWNLOAD_EXTRACT_TIMESTAMP`` option for controlling whether the timestamps
+  of extracted contents are set to match those in the archive when the ``URL``
+  download method is used. A new policy :policy:`CMP0135` was added to control
+  the default behavior when the new option is not used.

+ 56 - 4
Modules/ExternalProject.cmake

@@ -170,6 +170,19 @@ External Project Definition
         the default name is generally suitable and is not normally used outside
         the default name is generally suitable and is not normally used outside
         of code internal to the ``ExternalProject`` module.
         of code internal to the ``ExternalProject`` module.
 
 
+      ``DOWNLOAD_EXTRACT_TIMESTAMP <bool>``
+        .. versionadded:: 3.24
+
+        When specified with a true value, the timestamps of the extracted
+        files will match those in the archive. When false, the timestamps of
+        the extracted files will reflect the time at which the extraction
+        was performed. If the download URL changes, timestamps based off
+        those in the archive can result in dependent targets not being rebuilt
+        when they potentially should have been. Therefore, unless the file
+        timestamps are significant to the project in some way, use a false
+        value for this option. If ``DOWNLOAD_EXTRACT_TIMESTAMP`` is not given,
+        the default is false. See policy :policy:`CMP0135`.
+
       ``DOWNLOAD_NO_EXTRACT <bool>``
       ``DOWNLOAD_NO_EXTRACT <bool>``
         .. versionadded:: 3.6
         .. versionadded:: 3.6
 
 
@@ -1534,7 +1547,7 @@ function(_ep_write_verifyfile_script script_filename LOCAL hash)
 endfunction()
 endfunction()
 
 
 
 
-function(_ep_write_extractfile_script script_filename name filename directory)
+function(_ep_write_extractfile_script script_filename name filename directory options)
   set(args "")
   set(args "")
 
 
   if(filename MATCHES "(\\.|=)(7z|tar\\.bz2|tar\\.gz|tar\\.xz|tbz2|tgz|txz|zip)$")
   if(filename MATCHES "(\\.|=)(7z|tar\\.bz2|tar\\.gz|tar\\.xz|tbz2|tgz|txz|zip)$")
@@ -2761,16 +2774,51 @@ hash=${hash}
         )
         )
       endif()
       endif()
       list(APPEND cmd ${CMAKE_COMMAND} -P ${stamp_dir}/verify-${name}.cmake)
       list(APPEND cmd ${CMAKE_COMMAND} -P ${stamp_dir}/verify-${name}.cmake)
-      if (NOT no_extract)
+      get_target_property(extract_timestamp ${name} _EP_DOWNLOAD_EXTRACT_TIMESTAMP)
+      if(no_extract)
+        if(NOT extract_timestamp STREQUAL "extract_timestamp-NOTFOUND")
+          message(FATAL_ERROR
+            "Cannot specify DOWNLOAD_EXTRACT_TIMESTAMP when using "
+            "DOWNLOAD_NO_EXTRACT TRUE"
+          )
+        endif()
+        set_property(TARGET ${name} PROPERTY _EP_DOWNLOADED_FILE ${file})
+      else()
+        if(extract_timestamp STREQUAL "extract_timestamp-NOTFOUND")
+          # Default depends on policy CMP0135
+          if(_EP_CMP0135 STREQUAL "")
+            message(AUTHOR_WARNING
+              "The DOWNLOAD_EXTRACT_TIMESTAMP option was not given and policy "
+              "CMP0135 is not set. The policy's OLD behavior will be used. "
+              "When using a URL download, the timestamps of extracted files "
+              "should preferably be that of the time of extraction, otherwise "
+              "code that depends on the extracted contents might not be "
+              "rebuilt if the URL changes. The OLD behavior preserves the "
+              "timestamps from the archive instead, but this is usually not "
+              "what you want. Update your project to the NEW behavior or "
+              "specify the DOWNLOAD_EXTRACT_TIMESTAMP option with a value of "
+              "true to avoid this robustness issue."
+            )
+            set(extract_timestamp TRUE)
+          elseif(_EP_CMP0135 STREQUAL "NEW")
+            set(extract_timestamp FALSE)
+          else()
+            set(extract_timestamp TRUE)
+          endif()
+        endif()
+        if(extract_timestamp)
+          set(options "")
+        else()
+          set(options "--touch")
+        endif()
         _ep_write_extractfile_script(
         _ep_write_extractfile_script(
           "${stamp_dir}/extract-${name}.cmake"
           "${stamp_dir}/extract-${name}.cmake"
           "${name}"
           "${name}"
           "${file}"
           "${file}"
           "${source_dir}"
           "${source_dir}"
+          "${options}"
         )
         )
         list(APPEND cmd COMMAND ${CMAKE_COMMAND} -P ${stamp_dir}/extract-${name}.cmake)
         list(APPEND cmd COMMAND ${CMAKE_COMMAND} -P ${stamp_dir}/extract-${name}.cmake)
-      else ()
-        set_property(TARGET ${name} PROPERTY _EP_DOWNLOADED_FILE ${file})
       endif ()
       endif ()
     endif()
     endif()
   else()
   else()
@@ -3438,6 +3486,9 @@ function(ExternalProject_Add name)
       )
       )
     set(cmp0114 "NEW")
     set(cmp0114 "NEW")
   endif()
   endif()
+  cmake_policy(GET CMP0135 _EP_CMP0135
+    PARENT_SCOPE # undocumented, do not use outside of CMake
+    )
 
 
   _ep_get_configuration_subdir_suffix(cfgdir)
   _ep_get_configuration_subdir_suffix(cfgdir)
 
 
@@ -3483,6 +3534,7 @@ function(ExternalProject_Add name)
     URL_HASH
     URL_HASH
     URL_MD5
     URL_MD5
     DOWNLOAD_NAME
     DOWNLOAD_NAME
+    DOWNLOAD_EXTRACT_TIMESTAMP
     DOWNLOAD_NO_EXTRACT
     DOWNLOAD_NO_EXTRACT
     DOWNLOAD_NO_PROGRESS
     DOWNLOAD_NO_PROGRESS
     TIMEOUT
     TIMEOUT

+ 1 - 1
Modules/ExternalProject/extractfile.cmake.in

@@ -29,7 +29,7 @@ file(MAKE_DIRECTORY "${ut_dir}")
 # Extract it:
 # Extract it:
 #
 #
 message(STATUS "extracting... [tar @args@]")
 message(STATUS "extracting... [tar @args@]")
-execute_process(COMMAND ${CMAKE_COMMAND} -E tar @args@ ${filename}
+execute_process(COMMAND ${CMAKE_COMMAND} -E tar @args@ ${filename} @options@
   WORKING_DIRECTORY ${ut_dir}
   WORKING_DIRECTORY ${ut_dir}
   RESULT_VARIABLE rv
   RESULT_VARIABLE rv
 )
 )

+ 4 - 0
Source/cmPolicies.h

@@ -404,6 +404,10 @@ class cmMakefile;
   SELECT(POLICY, CMP0134,                                                     \
   SELECT(POLICY, CMP0134,                                                     \
          "Fallback to \"HOST\" Windows registry view when \"TARGET\" view "   \
          "Fallback to \"HOST\" Windows registry view when \"TARGET\" view "   \
          "is not usable.",                                                    \
          "is not usable.",                                                    \
+         3, 24, 0, cmPolicies::WARN)                                          \
+  SELECT(POLICY, CMP0135,                                                     \
+         "ExternalProject ignores timestamps in archives by default for the " \
+         "URL download method",                                               \
          3, 24, 0, cmPolicies::WARN)
          3, 24, 0, cmPolicies::WARN)
 
 
 #define CM_SELECT_ID(F, A1, A2, A3, A4, A5, A6) F(A1)
 #define CM_SELECT_ID(F, A1, A2, A3, A4, A5, A6) F(A1)

+ 18 - 0
Tests/RunCMake/CMP0135/CMP0135-Common.cmake

@@ -0,0 +1,18 @@
+include(ExternalProject)
+
+set(stamp_dir "${CMAKE_CURRENT_BINARY_DIR}/stamps")
+
+ExternalProject_Add(fake_ext_proj
+  # We don't actually do a build, so we never try to download from this URL
+  URL https://example.com/something.zip
+  STAMP_DIR ${stamp_dir}
+)
+
+# Report whether the --touch option was added to the extraction script
+set(extraction_script "${stamp_dir}/extract-fake_ext_proj.cmake")
+file(STRINGS "${extraction_script}" results REGEX "--touch")
+if("${results}" STREQUAL "")
+  message(STATUS "Using timestamps from archive")
+else()
+  message(STATUS "Using extraction time for the timestamps")
+endif()

+ 1 - 0
Tests/RunCMake/CMP0135/CMP0135-NEW-stdout.txt

@@ -0,0 +1 @@
+Using extraction time for the timestamps

+ 2 - 0
Tests/RunCMake/CMP0135/CMP0135-NEW.cmake

@@ -0,0 +1,2 @@
+cmake_policy(SET CMP0135 NEW)
+include(CMP0135-Common.cmake)

+ 1 - 0
Tests/RunCMake/CMP0135/CMP0135-OLD-stdout.txt

@@ -0,0 +1 @@
+Using timestamps from archive

+ 2 - 0
Tests/RunCMake/CMP0135/CMP0135-OLD.cmake

@@ -0,0 +1,2 @@
+cmake_policy(SET CMP0135 OLD)
+include(CMP0135-Common.cmake)

+ 10 - 0
Tests/RunCMake/CMP0135/CMP0135-WARN-stderr.txt

@@ -0,0 +1,10 @@
+CMake Warning \(dev\) at .*/Modules/ExternalProject.cmake:[0-9]+ \(message\):
+  The DOWNLOAD_EXTRACT_TIMESTAMP option was not given and policy CMP0135 is
+  not set\.  The policy's OLD behavior will be used\.  When using a URL
+  download, the timestamps of extracted files should preferably be that of
+  the time of extraction, otherwise code that depends on the extracted
+  contents might not be rebuilt if the URL changes\.  The OLD behavior
+  preserves the timestamps from the archive instead, but this is usually not
+  what you want\.  Update your project to the NEW behavior or specify the
+  DOWNLOAD_EXTRACT_TIMESTAMP option with a value of true to avoid this
+  robustness issue\.

+ 1 - 0
Tests/RunCMake/CMP0135/CMP0135-WARN-stdout.txt

@@ -0,0 +1 @@
+Using timestamps from archive

+ 2 - 0
Tests/RunCMake/CMP0135/CMP0135-WARN.cmake

@@ -0,0 +1,2 @@
+
+include(CMP0135-Common.cmake)

+ 3 - 0
Tests/RunCMake/CMP0135/CMakeLists.txt

@@ -0,0 +1,3 @@
+cmake_minimum_required(VERSION 3.23)
+project(${RunCMake_TEST} NONE)
+include(${RunCMake_TEST}.cmake)

+ 5 - 0
Tests/RunCMake/CMP0135/RunCMakeTest.cmake

@@ -0,0 +1,5 @@
+include(RunCMake)
+
+run_cmake(CMP0135-WARN)
+run_cmake(CMP0135-OLD)
+run_cmake(CMP0135-NEW)

+ 1 - 0
Tests/RunCMake/CMakeLists.txt

@@ -149,6 +149,7 @@ if("${CMAKE_C_COMPILER_ID}" STREQUAL "LCC" OR
 endif()
 endif()
 
 
 add_RunCMake_test(CMP0132)
 add_RunCMake_test(CMP0132)
+add_RunCMake_test(CMP0135)
 
 
 # The test for Policy 65 requires the use of the
 # The test for Policy 65 requires the use of the
 # CMAKE_SHARED_LIBRARY_LINK_CXX_FLAGS variable, which both the VS and Xcode
 # CMAKE_SHARED_LIBRARY_LINK_CXX_FLAGS variable, which both the VS and Xcode

+ 1 - 0
Tests/RunCMake/ExternalProject/Add_StepDependencies.cmake

@@ -4,6 +4,7 @@ if(CMAKE_XCODE_BUILD_SYSTEM VERSION_GREATER_EQUAL 12)
 else()
 else()
   cmake_policy(SET CMP0114 OLD) # Test deprecated behavior.
   cmake_policy(SET CMP0114 OLD) # Test deprecated behavior.
 endif()
 endif()
+cmake_policy(SET CMP0135 NEW)
 
 
 include(ExternalProject)
 include(ExternalProject)
 
 

+ 1 - 0
Tests/RunCMake/ExternalProject/Add_StepDependencies_no_target.cmake

@@ -4,6 +4,7 @@ if(CMAKE_XCODE_BUILD_SYSTEM VERSION_GREATER_EQUAL 12)
 else()
 else()
   cmake_policy(SET CMP0114 OLD) # Test deprecated behavior.
   cmake_policy(SET CMP0114 OLD) # Test deprecated behavior.
 endif()
 endif()
+cmake_policy(SET CMP0135 NEW)
 
 
 include(ExternalProject)
 include(ExternalProject)
 
 

+ 1 - 0
Tests/RunCMake/ExternalProject/CMakeLists.txt

@@ -3,4 +3,5 @@ project(${RunCMake_TEST} NONE)
 if(CMAKE_XCODE_BUILD_SYSTEM VERSION_GREATER_EQUAL 12 AND NOT RunCMake_TEST STREQUAL "Xcode-CMP0114")
 if(CMAKE_XCODE_BUILD_SYSTEM VERSION_GREATER_EQUAL 12 AND NOT RunCMake_TEST STREQUAL "Xcode-CMP0114")
   cmake_policy(SET CMP0114 NEW)
   cmake_policy(SET CMP0114 NEW)
 endif()
 endif()
+cmake_policy(SET CMP0135 NEW)
 include(${RunCMake_TEST}.cmake)
 include(${RunCMake_TEST}.cmake)

+ 1 - 0
Tests/RunCMake/ExternalProject/NO_DEPENDS-CMP0114-NEW-Direct.cmake

@@ -1,4 +1,5 @@
 cmake_policy(SET CMP0114 NEW)
 cmake_policy(SET CMP0114 NEW)
+cmake_policy(SET CMP0135 NEW)
 include(ExternalProject)
 include(ExternalProject)
 ExternalProject_Add(BAR SOURCE_DIR .  TEST_COMMAND echo test)
 ExternalProject_Add(BAR SOURCE_DIR .  TEST_COMMAND echo test)
 ExternalProject_Add_StepTargets(BAR NO_DEPENDS test)
 ExternalProject_Add_StepTargets(BAR NO_DEPENDS test)