string(TIMESTAMP): add %f specifier for microseconds

The %f specified extends the string(TIMESTAMP) and file(TIMESTAMP)
commands to output the timestamp with a microsecond resolution.
This convention is offered by python's datetime module.
Before, the precision was limited to seconds.

The implementation is done by extending existing cmTimestamp methods
with a `microseconds` parameter. This parameter is optional in order to
be backwards compatible. The timestamps are now received in a
cross-platform manner using libuv, since the standard C functions like
time() don't allow for sub-second precision.

This requires libuv 1.28 or higher.  We already require higher than
that on Windows, so update the required version for other platforms.

Implements: #19335
This commit is contained in:
Peter Würth 2022-01-27 15:20:15 +01:00 committed by Brad King
parent 44939f01e7
commit c050d6a01e
7 changed files with 68 additions and 14 deletions

View File

@ -655,7 +655,7 @@ macro (CMAKE_BUILD_UTILITIES)
if(WIN32) if(WIN32)
find_package(LibUV 1.38.0) find_package(LibUV 1.38.0)
else() else()
find_package(LibUV 1.10.0) find_package(LibUV 1.28.0)
endif() endif()
if(NOT LIBUV_FOUND) if(NOT LIBUV_FOUND)
message(FATAL_ERROR message(FATAL_ERROR

View File

@ -490,6 +490,9 @@ specifiers:
``%S`` ``%S``
The second of the current minute. 60 represents a leap second. (00-60) The second of the current minute. 60 represents a leap second. (00-60)
``%f``
The microsecond of the current second (000000-999999).
``%U`` ``%U``
The week number of the current year (00-53). The week number of the current year (00-53).

View File

@ -0,0 +1,5 @@
timestamp-microseconds
----------------------
* The :command:`string(TIMESTAMP)` and :command:`file(TIMESTAMP)` commands now
support the ``%f`` specifier for microseconds.

View File

@ -17,18 +17,27 @@
#include <cstdlib> #include <cstdlib>
#include <cstring> #include <cstring>
#include <sstream> #include <sstream>
#include <utility>
#ifdef __MINGW32__ #ifdef __MINGW32__
# include <libloaderapi.h> # include <libloaderapi.h>
#endif #endif
#include <cm3p/uv.h>
#include "cmStringAlgorithms.h" #include "cmStringAlgorithms.h"
#include "cmSystemTools.h" #include "cmSystemTools.h"
std::string cmTimestamp::CurrentTime(const std::string& formatString, std::string cmTimestamp::CurrentTime(const std::string& formatString,
bool utcFlag) const bool utcFlag) const
{ {
time_t currentTimeT = time(nullptr); // get current time with microsecond resolution
uv_timeval64_t timeval;
uv_gettimeofday(&timeval);
auto currentTimeT = static_cast<time_t>(timeval.tv_sec);
auto microseconds = static_cast<uint32_t>(timeval.tv_usec);
// check for override via SOURCE_DATE_EPOCH for reproducible builds
std::string source_date_epoch; std::string source_date_epoch;
cmSystemTools::GetEnv("SOURCE_DATE_EPOCH", source_date_epoch); cmSystemTools::GetEnv("SOURCE_DATE_EPOCH", source_date_epoch);
if (!source_date_epoch.empty()) { if (!source_date_epoch.empty()) {
@ -38,12 +47,15 @@ std::string cmTimestamp::CurrentTime(const std::string& formatString,
cmSystemTools::Error("Cannot parse SOURCE_DATE_EPOCH as integer"); cmSystemTools::Error("Cannot parse SOURCE_DATE_EPOCH as integer");
exit(27); exit(27);
} }
// SOURCE_DATE_EPOCH has only a resolution in the seconds range
microseconds = 0;
} }
if (currentTimeT == time_t(-1)) { if (currentTimeT == time_t(-1)) {
return std::string(); return std::string();
} }
return this->CreateTimestampFromTimeT(currentTimeT, formatString, utcFlag); return this->CreateTimestampFromTimeT(currentTimeT, microseconds,
formatString, utcFlag);
} }
std::string cmTimestamp::FileModificationTime(const char* path, std::string cmTimestamp::FileModificationTime(const char* path,
@ -57,13 +69,34 @@ std::string cmTimestamp::FileModificationTime(const char* path,
return std::string(); return std::string();
} }
time_t mtime = cmsys::SystemTools::ModifiedTime(real_path); // use libuv's implementation of stat(2) to get the file information
return this->CreateTimestampFromTimeT(mtime, formatString, utcFlag); time_t mtime = 0;
uint32_t microseconds = 0;
uv_fs_t req;
if (uv_fs_stat(nullptr, &req, real_path.c_str(), nullptr) == 0) {
mtime = static_cast<time_t>(req.statbuf.st_mtim.tv_sec);
// tv_nsec has nanosecond resolution, but we truncate it to microsecond
// resolution in order to be consistent with cmTimestamp::CurrentTime()
microseconds = static_cast<uint32_t>(req.statbuf.st_mtim.tv_nsec / 1000);
}
uv_fs_req_cleanup(&req);
return this->CreateTimestampFromTimeT(mtime, microseconds, formatString,
utcFlag);
} }
std::string cmTimestamp::CreateTimestampFromTimeT(time_t timeT, std::string cmTimestamp::CreateTimestampFromTimeT(time_t timeT,
std::string formatString, std::string formatString,
bool utcFlag) const bool utcFlag) const
{
return this->CreateTimestampFromTimeT(timeT, 0, std::move(formatString),
utcFlag);
}
std::string cmTimestamp::CreateTimestampFromTimeT(time_t timeT,
const uint32_t microseconds,
std::string formatString,
bool utcFlag) const
{ {
if (formatString.empty()) { if (formatString.empty()) {
formatString = "%Y-%m-%dT%H:%M:%S"; formatString = "%Y-%m-%dT%H:%M:%S";
@ -95,7 +128,8 @@ std::string cmTimestamp::CreateTimestampFromTimeT(time_t timeT,
: static_cast<char>(0); : static_cast<char>(0);
if (c1 == '%' && c2 != 0) { if (c1 == '%' && c2 != 0) {
result += this->AddTimestampComponent(c2, timeStruct, timeT); result +=
this->AddTimestampComponent(c2, timeStruct, timeT, microseconds);
++i; ++i;
} else { } else {
result += c1; result += c1;
@ -144,9 +178,9 @@ time_t cmTimestamp::CreateUtcTimeTFromTm(struct tm& tm) const
#endif #endif
} }
std::string cmTimestamp::AddTimestampComponent(char flag, std::string cmTimestamp::AddTimestampComponent(
struct tm& timeStruct, char flag, struct tm& timeStruct, const time_t timeT,
const time_t timeT) const const uint32_t microseconds) const
{ {
std::string formatString = cmStrCat('%', flag); std::string formatString = cmStrCat('%', flag);
@ -180,13 +214,19 @@ std::string cmTimestamp::AddTimestampComponent(char flag,
const time_t unixEpoch = this->CreateUtcTimeTFromTm(tmUnixEpoch); const time_t unixEpoch = this->CreateUtcTimeTFromTm(tmUnixEpoch);
if (unixEpoch == -1) { if (unixEpoch == -1) {
cmSystemTools::Error( cmSystemTools::Error(
"Error generating UNIX epoch in " "Error generating UNIX epoch in string(TIMESTAMP ...) or "
"STRING(TIMESTAMP ...). Please, file a bug report against CMake"); "file(TIMESTAMP ...). Please, file a bug report against CMake");
return std::string(); return std::string();
} }
return std::to_string(static_cast<long int>(difftime(timeT, unixEpoch))); return std::to_string(static_cast<long int>(difftime(timeT, unixEpoch)));
} }
case 'f': // microseconds
{
// clip number to 6 digits and pad with leading zeros
std::string microsecs = std::to_string(microseconds % 1000000);
return std::string(6 - microsecs.length(), '0') + microsecs;
}
default: { default: {
return formatString; return formatString;
} }

View File

@ -4,6 +4,7 @@
#include "cmConfigure.h" // IWYU pragma: keep #include "cmConfigure.h" // IWYU pragma: keep
#include <cstdint>
#include <ctime> #include <ctime>
#include <string> #include <string>
@ -23,9 +24,14 @@ public:
std::string CreateTimestampFromTimeT(time_t timeT, std::string formatString, std::string CreateTimestampFromTimeT(time_t timeT, std::string formatString,
bool utcFlag) const; bool utcFlag) const;
std::string CreateTimestampFromTimeT(time_t timeT, uint32_t microseconds,
std::string formatString,
bool utcFlag) const;
private: private:
time_t CreateUtcTimeTFromTm(struct tm& timeStruct) const; time_t CreateUtcTimeTFromTm(struct tm& timeStruct) const;
std::string AddTimestampComponent(char flag, struct tm& timeStruct, std::string AddTimestampComponent(char flag, struct tm& timeStruct,
time_t timeT) const; time_t timeT,
uint32_t microseconds = 0) const;
}; };

View File

@ -1 +1 @@
RESULT=2005-08-07 23:19:49 Sunday=Sun August=Aug 05 day=219 wd=0 week=32 w_iso=31 %I=11 epoch=1123456789 RESULT=2005-08-07 23:19:49.000000 Sunday=Sun August=Aug 05 day=219 wd=0 week=32 w_iso=31 %I=11 epoch=1123456789

View File

@ -1,3 +1,3 @@
set(ENV{SOURCE_DATE_EPOCH} "1123456789") set(ENV{SOURCE_DATE_EPOCH} "1123456789")
string(TIMESTAMP RESULT "%Y-%m-%d %H:%M:%S %A=%a %B=%b %y day=%j wd=%w week=%U w_iso=%V %%I=%I epoch=%s" UTC) string(TIMESTAMP RESULT "%Y-%m-%d %H:%M:%S.%f %A=%a %B=%b %y day=%j wd=%w week=%U w_iso=%V %%I=%I epoch=%s" UTC)
message("RESULT=${RESULT}") message("RESULT=${RESULT}")