瀏覽代碼

CPack/Deb: Support SOURCE_DATE_EPOCH when packaging files

Andrew Fuller 7 年之前
父節點
當前提交
548ac51d8e

+ 8 - 0
Help/cpack_gen/deb.rst

@@ -527,3 +527,11 @@ alternate data stream (ADT) is used.
 
 When a filesystem without ADT support is used only owner read/write
 permissions can be preserved.
+
+Reproducible packages
+^^^^^^^^^^^^^^^^^^^^^
+
+The environment variable ``SOURCE_DATE_EPOCH`` may be set to a UNIX
+timestamp, defined as the number of seconds, excluding leap seconds,
+since 01 Jan 1970 00:00:00 UTC.  If set, the CPack Deb generator will
+use its value for timestamps in the package.

+ 6 - 0
Help/release/dev/cpack-deb-source-date-epoch.rst

@@ -0,0 +1,6 @@
+cpack-deb-source-date-epoch
+---------------------------
+
+* The :cpack_gen:`CPack Deb Generator` learned to honor the ``SOURCE_DATE_EPOCH``
+  environment variable when packaging files.  This is useful for generating
+  reproducible packages.

+ 26 - 2
Source/cmArchiveWrite.cxx

@@ -10,6 +10,7 @@
 #include "cmsys/Encoding.hxx"
 #include "cmsys/FStream.hxx"
 #include <iostream>
+#include <sstream>
 #include <string.h>
 #include <time.h>
 
@@ -94,13 +95,25 @@ cmArchiveWrite::cmArchiveWrite(std::ostream& os, Compress c,
         return;
       }
       break;
-    case CompressGZip:
+    case CompressGZip: {
       if (archive_write_add_filter_gzip(this->Archive) != ARCHIVE_OK) {
         this->Error = "archive_write_add_filter_gzip: ";
         this->Error += cm_archive_error_string(this->Archive);
         return;
       }
-      break;
+      std::string source_date_epoch;
+      cmSystemTools::GetEnv("SOURCE_DATE_EPOCH", source_date_epoch);
+      if (!source_date_epoch.empty()) {
+        // We're not able to specify an arbitrary timestamp for gzip.
+        // The next best thing is to omit the timestamp entirely.
+        if (archive_write_set_filter_option(this->Archive, "gzip", "timestamp",
+                                            nullptr) != ARCHIVE_OK) {
+          this->Error = "archive_write_set_filter_option: ";
+          this->Error += cm_archive_error_string(this->Archive);
+          return;
+        }
+      }
+    } break;
     case CompressBZip2:
       if (archive_write_add_filter_bzip2(this->Archive) != ARCHIVE_OK) {
         this->Error = "archive_write_add_filter_bzip2: ";
@@ -243,6 +256,17 @@ bool cmArchiveWrite::AddFile(const char* file, size_t skip, const char* prefix)
       return false;
     }
     archive_entry_set_mtime(e, t, 0);
+  } else {
+    std::string source_date_epoch;
+    cmSystemTools::GetEnv("SOURCE_DATE_EPOCH", source_date_epoch);
+    if (!source_date_epoch.empty()) {
+      std::istringstream iss(source_date_epoch);
+      time_t epochTime;
+      iss >> epochTime;
+      if (iss.eof() && !iss.fail()) {
+        archive_entry_set_mtime(e, epochTime, 0);
+      }
+    }
   }
 
   // manages the uid/guid of the entry (if any)

+ 5 - 1
Tests/RunCMake/CPack/CPackTestHelpers.cmake

@@ -78,8 +78,12 @@ function(run_cpack_test_common_ TEST_NAME types build SUBTEST_SUFFIX source PACK
     endif()
 
     # execute cpack
+    set(SETENV)
+    if(ENVIRONMENT)
+      set(SETENV ${CMAKE_COMMAND} -E env "${ENVIRONMENT}")
+    endif()
     execute_process(
-      COMMAND ${cpack_command_}
+      COMMAND ${SETENV} ${cpack_command_}
       WORKING_DIRECTORY "${RunCMake_TEST_BINARY_DIR}"
       RESULT_VARIABLE "result_"
       OUTPUT_FILE "${RunCMake_TEST_BINARY_DIR}/test_output.txt"

+ 1 - 1
Tests/RunCMake/CPack/DEB/Helpers.cmake

@@ -1,7 +1,7 @@
 set(ALL_FILES_GLOB "*.deb")
 
 function(getPackageContent FILE RESULT_VAR)
-  execute_process(COMMAND ${DPKG_EXECUTABLE} -c "${FILE}"
+  execute_process(COMMAND ${CMAKE_COMMAND} -E env TZ=Etc/UTC ${DPKG_EXECUTABLE} -c "${FILE}"
           OUTPUT_VARIABLE package_content_
           ERROR_QUIET
           OUTPUT_STRIP_TRAILING_WHITESPACE)

+ 3 - 0
Tests/RunCMake/CPack/RunCMakeTest.cmake

@@ -28,6 +28,9 @@ run_cpack_test(EXTRA_SLASH_IN_PATH "RPM" true "COMPONENT")
 run_cpack_source_test(SOURCE_PACKAGE "RPM")
 run_cpack_test(SUGGESTS "RPM" false "MONOLITHIC")
 run_cpack_test(SYMLINKS "RPM;TGZ" false "MONOLITHIC;COMPONENT")
+set(ENVIRONMENT "SOURCE_DATE_EPOCH=123456789")
+run_cpack_test(TIMESTAMPS "DEB;TGZ" false "COMPONENT")
+unset(ENVIRONMENT)
 run_cpack_test(USER_FILELIST "RPM" false "MONOLITHIC")
 run_cpack_test(MD5SUMS "DEB" false "MONOLITHIC;COMPONENT")
 run_cpack_test(CPACK_INSTALL_SCRIPT "ZIP" false "MONOLITHIC")

+ 2 - 0
Tests/RunCMake/CPack/tests/TIMESTAMPS/ExpectedFiles.cmake

@@ -0,0 +1,2 @@
+set(EXPECTED_FILES_COUNT "1")
+set(EXPECTED_FILE_CONTENT_1_LIST "/foo;/foo/CMakeLists.txt")

+ 58 - 0
Tests/RunCMake/CPack/tests/TIMESTAMPS/VerifyResult.cmake

@@ -0,0 +1,58 @@
+macro(getFileMetadata_ FILE RESULT_VAR)
+  if(GENERATOR_TYPE STREQUAL "TGZ")
+    # getPackageContent defined for archives omit the metadata (non-verbose)
+    execute_process(COMMAND ${CMAKE_COMMAND} -E env TZ=Etc/UTC ${CMAKE_COMMAND} -E tar -xtvf ${FILE}
+            OUTPUT_VARIABLE ${RESULT_VAR}
+            ERROR_QUIET
+            OUTPUT_STRIP_TRAILING_WHITESPACE)
+  else()
+    getPackageContent("${FILE}" ${RESULT_VAR})
+  endif()
+endmacro()
+
+function(checkContentTimestamp FILE REGEX)
+  getFileMetadata_("${FILE}" METADATA_)
+
+  if(NOT METADATA_ MATCHES "${REGEX}")
+    string(REPLACE "\n" "\n  " metadata_indented "${METADATA_}")
+    message(FATAL_ERROR
+      "Wrong timestamps in file:\n"
+      "  ${FILE}\n"
+      "Expected timestamps to match:\n"
+      "  ${REGEX}\n"
+      "Actual timestamps:\n"
+      "  ${metadata_indented}")
+  endif()
+endfunction()
+
+function(checkTimestamp FILE_NAME)
+  file(READ ${FILE_NAME} ACTUAL_TIMESTAMP OFFSET 4 LIMIT 4 HEX)
+
+  if(NOT ACTUAL_TIMESTAMP STREQUAL "00000000")
+    message(FATAL_ERROR "${FILE_NAME} contains a timestamp [0x${ACTUAL_TIMESTAMP}]")
+  endif()
+endfunction()
+
+# Expected timestamp is UNIX time 123456789
+if(GENERATOR_TYPE STREQUAL "TGZ")
+  set(EXPECTED_TIMESTAMP "29 Nov +1973")
+  set(EXPECTED_FILES foo/ foo/CMakeLists.txt)
+else()
+  set(EXPECTED_TIMESTAMP "1973-11-29 21:33")
+  set(EXPECTED_FILES ./usr/ ./usr/foo/ ./usr/foo/CMakeLists.txt)
+endif()
+
+set(EXPECTED_METADATA)
+foreach(FILE ${EXPECTED_FILES})
+  list(APPEND EXPECTED_METADATA ".* ${EXPECTED_TIMESTAMP} ${FILE}")
+endforeach()
+list(JOIN EXPECTED_METADATA ".*" EXPECTED_REGEX)
+checkContentTimestamp("${FOUND_FILE_1}" "${EXPECTED_REGEX}")
+
+if(GENERATOR_TYPE STREQUAL "TGZ")
+  checkTimestamp("${FOUND_FILE_1}")
+else()
+  execute_process(COMMAND ${CMAKE_COMMAND} -E tar xf "${FOUND_FILE_1}")
+  checkTimestamp("data.tar.gz")
+  checkTimestamp("control.tar.gz")
+endif()

+ 3 - 0
Tests/RunCMake/CPack/tests/TIMESTAMPS/test.cmake

@@ -0,0 +1,3 @@
+install(FILES CMakeLists.txt DESTINATION foo COMPONENT test)
+
+set(CPACK_COMPONENTS_ALL test)