利用CMake自动为程序嵌入Git、Date相关信息

前排描述

本文章基于linux(Ubuntu)环境,主要描述:

1、为了方便程序的版本控制追踪,利用CMake自动生成保存有例如:

  • git当前用户名
  • git分支名
  • git log中最新提交记录的hash、提交者、提交者邮箱地址、提交时间
  • cmake构建的时间

2、利用CMake自定义命令,调用shell脚本,实现编译时间的嵌入

3、全自动化操作,工程编译只需要正常cmake与make,无额外人工操作

最终将这些信息保存到一个.h头文件中,然后程序中引用该头文件,调用相关的信息并打印出来


可能有人会说,为什么不直接使用例如C、C++中提供的__DATE__、__TIME__宏定义来实现编译时间的打印?

实际这确实是可行的,但这仅限于单源代码文件的工程

当工程中不止一个源代码文件,假如将这个打印加入在a.cpp的main()函数中,但是在某一次修改中,修改了其他源代码文件(例如b.cpp),没有修改main()函数所处的a.cpp源代码文件,这时调用make编译工程,因a.cpp没有被修改,make将跳过编译a.cpp(假定上一次已经完整编译并且没有删除相关中间文件),导致main()函数中的打印编译时间没有被改变,并不能真正实现编译时间的记录

因此,本文章中提供了一种实现方法供参考,这将保存每一次make操作时的时间作为编译时间(当然,需要有任意源文件被修改的情况下make🙂)

本篇文章中将使用C++语言作举例

代码

这是用来生成储存相关参数.h头文件的配置文件(此处命名为xxx.h.ini)

CMake构建中将基于此文件自动生成.h文件

其中的静态变量定义将在CMake和Make操作中自动填充

  • 首先会在CMake时将“@…@”中的字符串替换为对应的参数
  • 然后在make操作中执行对应的脚本,将”$$$…$$$”中的字符串替换为执行make时的系统时间

make操作中替换后将会是“$$$2022-12$$$”(例子),这是为了下一次的替换做准备,因此在程序调用中需要删除全部’$’字符,才是真正的时间字符串

#ifndef _GIT_VERSION_DATA_
#define _GIT_VERSION_DATA_

#include <string>

//本文件由程序自动生成,用于将编译信息嵌入程序,请勿作任何修改!

//cmake时自动生成
const std::string VERSION_DATA_GIT_USER = "@GIT_USER@";
const std::string VERSION_DATA_GIT_BRANCH = "@GIT_BRANCH@";
const std::string VERSION_DATA_GIT_VERSION = "@GIT_VERSION@";
const std::string VERSION_DATA_GIT_TIME = "@GIT_DATETIME@";
const std::string VERSION_DATA_GIT_COMMIT_USER = "@GIT_COMMIT_USER@";
const std::string VERSION_DATA_GIT_COMMIT_USER_EMAIL = "@GIT_COMMIT_USER_EMAIL@";
const std::string VERSION_DATA_CMAKE_TIME = "@BUILD_DATETIME@";

//make时自动生成
const std::string VERSION_DATA_COMPILE_TIME = "$$$REPLACE_COMPILE_TIME$$$";

#endif

以下代码是需要在CMakeList.txt中添加的命令代码

这一部分主要是在获取git相关的信息,然后根据上述的.h.ini配置文件生成对应的.h嵌入参数头文件,其中的相应字符串将被替换为对应的字符串数据

  • git当前用户名
  • git分支名
  • git log中最新提交记录的hash、提交者、提交者邮箱地址、提交时间
  • cmake构建的时间
execute_process(COMMAND	git config user.name WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} OUTPUT_VARIABLE GIT_USER)
execute_process(COMMAND	git branch WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} OUTPUT_FILE ".TMP_GIT_BRANCH.tmp")
execute_process(COMMAND	grep "*" ${PROJECT_SOURCE_DIR}/.TMP_GIT_BRANCH.tmp WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} OUTPUT_VARIABLE GIT_BRANCH)
execute_process(COMMAND	rm -rf ${PROJECT_SOURCE_DIR}/.TMP_GIT_BRANCH.tmp)
execute_process(COMMAND	git log -1 --format="%H" WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} OUTPUT_VARIABLE GIT_VERSION)
execute_process(COMMAND	git log -1 --format="%ct" WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} OUTPUT_VARIABLE GIT_TIME)
execute_process(COMMAND	git log -1 --format="%cn" WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} OUTPUT_VARIABLE GIT_COMMIT_USER)
execute_process(COMMAND	git log -1 --format="%ce" WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} OUTPUT_VARIABLE GIT_COMMIT_USER_EMAIL)string (REGEX REPLACE "[\n\t\r\"]" "" GIT_TIME ${GIT_TIME})
execute_process(COMMAND date -d@${GIT_TIME} +"%Y-%m-%d %H:%M:%S" WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} OUTPUT_VARIABLE GIT_DATETIME)
execute_process(COMMAND date +"%Y-%m-%d %H:%M:%S" WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} OUTPUT_VARIABLE BUILD_DATETIME)
# 对得到的结果进行处理,尤其注意 \n,\t,\r之类的特殊字符,在cmake时没问题,但是生成的.cmake文件有问题,导致make出错。
string (REGEX REPLACE "[\n\t\r]" "" GIT_USER ${GIT_USER})
string (REGEX REPLACE "[\n\t\r]" "" GIT_BRANCH ${GIT_BRANCH})
string (REGEX REPLACE "[\n\t\r]" "" GIT_VERSION ${GIT_VERSION})
string (REGEX REPLACE "[\n\t\r]" "" GIT_DATETIME ${GIT_DATETIME})
string (REGEX REPLACE "[\n\t\r]" "" GIT_COMMIT_USER ${GIT_COMMIT_USER})
string (REGEX REPLACE "[\n\t\r]" "" GIT_COMMIT_USER_EMAIL ${GIT_COMMIT_USER_EMAIL})
string (REGEX REPLACE "[\n\t\r]" "" BUILD_DATETIME ${BUILD_DATETIME})
# 去掉“”
string (REGEX REPLACE "\"" "" GIT_VERSION ${GIT_VERSION})
string (REGEX REPLACE "\"" "" GIT_DATETIME ${GIT_DATETIME})
string (REGEX REPLACE "\"" "" GIT_COMMIT_USER ${GIT_COMMIT_USER})
string (REGEX REPLACE "\"" "" GIT_COMMIT_USER_EMAIL ${GIT_COMMIT_USER_EMAIL})
string (REGEX REPLACE "\"" "" BUILD_DATETIME ${BUILD_DATETIME})
# 去掉"* "
string (REPLACE "* " "" GIT_BRANCH ${GIT_BRANCH})
message("**************************************************************")
message("* Git Branch:" ${GIT_BRANCH})
message("* Git Commit:" ${GIT_VERSION})
message("* Git Commit Time:" ${GIT_DATETIME})
message("* Git Commit User:" ${GIT_COMMIT_USER} "(" ${GIT_COMMIT_USER_EMAIL} ")")

message("* ")
message("* Git User:" ${GIT_USER})
message("* Cmake Time:" ${BUILD_DATETIME})
message("**************************************************************")
# 创建包含git信息的.h文件供程序调用
configure_file(
  ${PROJECT_SOURCE_DIR}/AuxrFile/GitVersionData.h.ini
  ${PROJECT_SOURCE_DIR}/include/GitVersionData.h
  @ONLY
  )

以下代码是需要在CMakeList.txt中添加的命令代码

这一部分的操作目的是添加自定义命令

用于make操作时自动执行shell脚本来实现将编译时间字符串替换进.h嵌入参数头文件中

#make自定义命令,用于记录编译时间
add_custom_command(
  OUTPUT TmpReplaceFile
  COMMAND echo "Rreparation Replace File..."
  )
add_custom_target(
  ReplaceFile ALL
  DEPENDS TmpReplaceFile
  )
add_custom_command(
  TARGET ReplaceFile
  PRE_BUILD
  COMMAND ./AuxrFile/ReplaceTimeData.sh
  COMMENT "Now Replace File..."
  VERBATIM
  )

以下代码是.sh shell脚本代码

脚本主要是为了实现获取编译时间并将时间嵌入程序中

其将在make操作中被最先执行

  • 首先获取系统当前时间
  • 将时间字符串替换cmake自动生成的.h文件中的编译时间字符串
  • 执行正常的make代码编译流程

这样就能实现,只要工程中有新的编译操作,程序中嵌入的编译时间将会伴随更新

COMPILE_TIME=$(date "+%Y-%m-%d %H:%M:%S")
echo 'COMPILE_TIME:'${COMPILE_TIME}
sed -i 's/\$\$\$.*\$\$\$/\$\$\$'"${COMPILE_TIME}"'\$\$\$/g' ./include/GitVersionData.h

在程序中调用.h嵌入参数头文件,利用相关生成的参数来打印出参数

此处为伪代码,仅供参考,请根据实际情况修改

这里可以放在例如main()函数中,让程序每一次执行都将相关参数打印出来供版本控制与追踪

#include "GitVersionData.h"

static const char *Code_Version = "1.0.0";//程序版本号

string tmpstr = VERSION_DATA_COMPILE_TIME;//获取编译时间字符串
tmpstr.erase(std::remove(tmpstr.begin(), tmpstr.end(), '$'), tmpstr.end());//将字符串中的所有$字符删除
printf(
"\n********************************************\n"\
"* ProgramName\n"\
"* Program Version:%s\n"
"* Cmake Time:%s\n"
"* Compile Time:%s\n"\
"* Compile Author:%s\n"\
"* \n"\
"* Git Branch:%s\n"\
"* Git Commit:%s\n"\
"* Git Commit Time:%s\n"\
"* Git Commit User:%s(%s)\n"\
"********************************************\n"
, Code_Version, VERSION_DATA_CMAKE_TIME.c_str(),  tmpstr.c_str(), VERSION_DATA_GIT_USER.c_str()
, VERSION_DATA_GIT_BRANCH.c_str(), VERSION_DATA_GIT_VERSION.c_str(), VERSION_DATA_GIT_TIME.c_str()
, VERSION_DATA_GIT_COMMIT_USER.c_str(), VERSION_DATA_GIT_COMMIT_USER_EMAIL.c_str()
);

后语

最终是实现了想要达到的需求,但可能还算是比较繁琐

例如还需要单独写一个shell脚本文件,作者本打算直接在CMake自定义命令中实现类似的做法,但却报错了,所以折中通过shell脚本来实现,若读者有更好的方法,欢迎评论交流😀

实际也可以通过删除信息打印命令所在源代码文件的中间文件(如.o文件),因为丢失了中间文件,这样make操作中将会重新对源文件进行编译

注意:本文章中多数代码都是不能直接复制粘贴使用的,例如有文件相关的操作,需要根据实际情况修改

参考

部分代码参考于互联网,本文章做了总结与实际测试,提供了一个可行的方案供读者参考

因忘记链接待添加…

指针是什么意思