diff --git a/Help/command/string.rst b/Help/command/string.rst index 0361c74db..3f4050ed5 100644 --- a/Help/command/string.rst +++ b/Help/command/string.rst @@ -277,6 +277,7 @@ specifiers: %j The day of the current year (001-366). %m The month of the current year (01-12). %M The minute of the current hour (00-59). + %s Seconds since midnight (UTC) 1-Jan-1970 (UNIX time). %S The second of the current minute. 60 represents a leap second. (00-60) %U The week number of the current year (00-53). diff --git a/Help/release/dev/unix-timestamps.rst b/Help/release/dev/unix-timestamps.rst new file mode 100644 index 000000000..cdb0e5bf9 --- /dev/null +++ b/Help/release/dev/unix-timestamps.rst @@ -0,0 +1,6 @@ +unix-timestamps +--------------- + +* The :command:`string(TIMESTAMP)` and :command:`file(TIMESTAMP)` + commands gained support for the ``%s`` placeholder. This is + the number of seconds since the UNIX Epoch. diff --git a/Source/cmTimestamp.cxx b/Source/cmTimestamp.cxx index 6fd6ab7b4..1c795c4ff 100644 --- a/Source/cmTimestamp.cxx +++ b/Source/cmTimestamp.cxx @@ -12,9 +12,11 @@ #include "cmTimestamp.h" #include +#include #include #include +#include //---------------------------------------------------------------------------- std::string cmTimestamp::CurrentTime( @@ -44,7 +46,7 @@ std::string cmTimestamp::FileModificationTime(const char* path, //---------------------------------------------------------------------------- std::string cmTimestamp::CreateTimestampFromTimeT(time_t timeT, - std::string formatString, bool utcFlag) + std::string formatString, bool utcFlag) const { if(formatString.empty()) { @@ -79,12 +81,12 @@ std::string cmTimestamp::CreateTimestampFromTimeT(time_t timeT, for(std::string::size_type i = 0; i < formatString.size(); ++i) { char c1 = formatString[i]; - char c2 = (i+1 < formatString.size()) ? - formatString[i+1] : static_cast(0); + char c2 = (i + 1 < formatString.size()) ? + formatString[i + 1] : static_cast(0); if(c1 == '%' && c2 != 0) { - result += AddTimestampComponent(c2, timeStruct); + result += AddTimestampComponent(c2, timeStruct, timeT); ++i; } else @@ -96,9 +98,41 @@ std::string cmTimestamp::CreateTimestampFromTimeT(time_t timeT, return result; } +//---------------------------------------------------------------------------- +time_t cmTimestamp::CreateUtcTimeTFromTm(struct tm &tm) const +{ +#if defined(_MSC_VER) && _MSC_VER >= 1400 + return _mkgmtime(&tm); +#else + // From Linux timegm() manpage. + + std::string tz_old = "TZ="; + if (const char* tz = cmSystemTools::GetEnv("TZ")) + { + tz_old += tz; + } + + // The standard says that "TZ=" or "TZ=[UNRECOGNIZED_TZ]" means UTC. + // It seems that "TZ=" does NOT work, at least under Windows + // with neither MSVC nor MinGW, so let's use explicit "TZ=UTC" + + cmSystemTools::PutEnv("TZ=UTC"); + + tzset(); + + time_t result = mktime(&tm); + + cmSystemTools::PutEnv(tz_old); + + tzset(); + + return result; +#endif +} + //---------------------------------------------------------------------------- std::string cmTimestamp::AddTimestampComponent( - char flag, struct tm& timeStruct) + char flag, struct tm& timeStruct, const time_t timeT) const { std::string formatString = "%"; formatString += flag; @@ -117,6 +151,26 @@ std::string cmTimestamp::AddTimestampComponent( case 'y': case 'Y': break; + case 's': // Seconds since UNIX epoch (midnight 1-jan-1970) + { + // Build a time_t for UNIX epoch and substract from the input "timeT": + struct tm tmUnixEpoch; + memset(&tmUnixEpoch, 0, sizeof(tmUnixEpoch)); + tmUnixEpoch.tm_mday = 1; + tmUnixEpoch.tm_year = 1970-1900; + + const time_t unixEpoch = this->CreateUtcTimeTFromTm(tmUnixEpoch); + if (unixEpoch == -1) + { + cmSystemTools::Error("Error generating UNIX epoch in " + "STRING(TIMESTAMP ...). Please, file a bug report aginst CMake"); + return std::string(); + } + + std::stringstream ss; + ss << static_cast(difftime(timeT, unixEpoch)); + return ss.str(); + } default: { return formatString; diff --git a/Source/cmTimestamp.h b/Source/cmTimestamp.h index 24c186983..7c4b2166e 100644 --- a/Source/cmTimestamp.h +++ b/Source/cmTimestamp.h @@ -16,7 +16,7 @@ #include /** \class cmTimestamp - * \brief Utility class to generate sting representation of a timestamp + * \brief Utility class to generate string representation of a timestamp * */ class cmTimestamp @@ -30,10 +30,13 @@ public: const std::string& formatString, bool utcFlag); private: - std::string CreateTimestampFromTimeT(time_t timeT, - std::string formatString, bool utcFlag); + time_t CreateUtcTimeTFromTm(struct tm& timeStruct) const; - std::string AddTimestampComponent(char flag, struct tm& timeStruct); + std::string CreateTimestampFromTimeT( + time_t timeT, std::string formatString, bool utcFlag) const; + + std::string AddTimestampComponent( + char flag, struct tm& timeStruct, time_t timeT) const; }; diff --git a/Tests/CMakeTests/String-TIMESTAMP-UnixTime.cmake b/Tests/CMakeTests/String-TIMESTAMP-UnixTime.cmake new file mode 100644 index 000000000..a93e7f546 --- /dev/null +++ b/Tests/CMakeTests/String-TIMESTAMP-UnixTime.cmake @@ -0,0 +1,22 @@ +string(TIMESTAMP timestamp "[%Y-%m-%d %H:%M:%S] %s" UTC) + +string(TIMESTAMP unix_time "%s") + +string(TIMESTAMP year "%Y" UTC) +string(TIMESTAMP days "%j" UTC) + +# Doing proper date calculations here to verify unix timestamps +# could be error prone. +# At the very least use some safe lower and upper bounds to +# see if we are somewhere in the right region. + +math(EXPR years_since_epoch "${year} - 1970") +math(EXPR lower_bound "((${years_since_epoch} * 365) + ${days}) * 86400") +math(EXPR upper_bound "((${years_since_epoch} * 366) + ${days}) * 86400") + + +if(unix_time GREATER lower_bound AND unix_time LESS upper_bound) + message("~${unix_time}~") +else() + message(FATAL_ERROR "${timestamp} unix time not in expected range [${lower_bound}, ${upper_bound}]") +endif() diff --git a/Tests/CMakeTests/StringTest.cmake.in b/Tests/CMakeTests/StringTest.cmake.in index 92e70c3ac..aba35fe5f 100644 --- a/Tests/CMakeTests/StringTest.cmake.in +++ b/Tests/CMakeTests/StringTest.cmake.in @@ -36,6 +36,8 @@ set(TIMESTAMP-IncompleteSpecifier-RESULT 0) set(TIMESTAMP-IncompleteSpecifier-STDERR "~foobar%~") set(TIMESTAMP-AllSpecifiers-RESULT 0) set(TIMESTAMP-AllSpecifiers-STDERR "~[0-9]+(;[0-9]+)*~") +set(TIMESTAMP-UnixTime-RESULT 0) +set(TIMESTAMP-UnixTime-STDERR "~[1-9][0-9]+~") include("@CMAKE_CURRENT_SOURCE_DIR@/CheckCMakeTest.cmake") check_cmake_test(String @@ -58,6 +60,7 @@ check_cmake_test(String TIMESTAMP-UnknownSpecifier TIMESTAMP-IncompleteSpecifier TIMESTAMP-AllSpecifiers + TIMESTAMP-UnixTime ) # Execute each test listed in StringTestScript.cmake: @@ -68,9 +71,12 @@ set(number_of_tests_expected 70) include("@CMAKE_CURRENT_SOURCE_DIR@/ExecuteScriptTests.cmake") execute_all_script_tests(${scriptname} number_of_tests_executed) +string(TIMESTAMP timestamp "[%Y-%m-%d %H:%M:%S] UTC %s" UTC) + # And verify that number_of_tests_executed is at least as many as we know # about as of this writing... # +message(STATUS "timestamp='${timestamp}'") message(STATUS "scriptname='${scriptname}'") message(STATUS "number_of_tests_executed='${number_of_tests_executed}'") message(STATUS "number_of_tests_expected='${number_of_tests_expected}'")