如何使用cmake将git SHA1作为定义传递给编译器?

在一个Makefile中,这可以通过如下方式来完成:

g++ -DGIT_SHA1="`git log -1 | head -n 1`" ... 

这是非常有用的,因为二进制文件知道确切的提交SHA1,所以它可以转储它的情况下,段错误。

我怎样才能达到与CMake相同?

我已经做了一些CMake模块,为了版本pipe理和类似的目的而进入一个git仓库 – 它们都在我的仓库中,位于https://github.com/rpavlik/cmake-modules

关于这些函数的好处在于,每次HEAD提交更改时,它们都会在构build之前强制重新configuration(重新运行cmake)。 与使用execute_process只执行一次操作不同,您不需要记住重新更新哈希定义。

为了这个特定的目的,你至less需要GetGitRevisionDescription.cmakeGetGitRevisionDescription.cmake.in文件。 然后,在你的主CMakeLists.txt文件中,你会有这样的东西

 list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/whereYouPutMyModules/") include(GetGitRevisionDescription) get_git_head_revision(GIT_REFSPEC GIT_SHA1) 

然后,您可以将其添加为系统范围的定义(不幸的是会导致大量的重build)

 add_definitions("-DGIT_SHA1=${GIT_SHA1}") 

或者,我build议的替代方法:生成一个生成的源文件。 在你的源代码中创build这两个文件:

GitSHA1.cpp.in:

 #define GIT_SHA1 "@GIT_SHA1@" const char g_GIT_SHA1[] = GIT_SHA1; 

GitSHA1.h:

 extern const char g_GIT_SHA1[]; 

把这个添加到你的CMakeLists.txt (假设你在源文件中有一个源文件列表):

 configure_file("${CMAKE_CURRENT_SOURCE_DIR}/GitSHA1.cpp.in" "${CMAKE_CURRENT_BINARY_DIR}/GitSHA1.cpp" @ONLY) list(APPEND SOURCES "${CMAKE_CURRENT_BINARY_DIR}/GitSHA1.cpp" GitSHA1.h) 

然后,你有一个包含你的SHAstring的全局variables – 当SHA执行时,带有extern的头文件不会改变,所以你可以只包含你想要引用string的任何地方,然后只有生成的CPP需要每次提交时都要重新编译,以便您访问各处的SHA。

我会用。 像我在CMakeLists.txt中一样:

 exec_program( "git" ${CMAKE_CURRENT_SOURCE_DIR} ARGS "describe" OUTPUT_VARIABLE VERSION ) string( REGEX MATCH "-g.*$" VERSION_SHA1 ${VERSION} ) string( REGEX REPLACE "[-g]" "" VERSION_SHA1 ${VERSION_SHA1} ) add_definitions( -DGIT_SHA1="${VERSION_SHA1}" ) 

我这样做是为了产生:

 const std::string Version::GIT_SHA1 = "e7fb69fb8ee93ac66f006406781138562d0250fb"; const std::string Version::GIT_DATE = "Thu Jan 9 14:17:56 2014"; const std::string Version::GIT_COMMIT_SUBJECT = "Fix all the bugs"; 

如果执行构build的工作空间具有未提交的更改,则上面的SHA1string将以-dirty作为后缀。

CMakeLists.txt

 # the commit's SHA1, and whether the building workspace was dirty or not execute_process(COMMAND "${GIT_EXECUTABLE}" describe --match=NeVeRmAtCh --always --abbrev=40 --dirty WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" OUTPUT_VARIABLE GIT_SHA1 ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) # the date of the commit execute_process(COMMAND "${GIT_EXECUTABLE}" log -1 --format=%ad --date=local WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" OUTPUT_VARIABLE GIT_DATE ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) # the subject of the commit execute_process(COMMAND "${GIT_EXECUTABLE}" log -1 --format=%s WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" OUTPUT_VARIABLE GIT_COMMIT_SUBJECT ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) # generate version.cc configure_file("${CMAKE_CURRENT_SOURCE_DIR}/version.cc.in" "${CMAKE_CURRENT_BINARY_DIR}/version.cc" @ONLY) list(APPEND SOURCES "${CMAKE_CURRENT_BINARY_DIR}/version.cc" version.hh) 

这需要version.cc.in

 #include "version.hh" using namespace my_app; const std::string Version::GIT_SHA1 = "@GIT_SHA1@"; const std::string Version::GIT_DATE = "@GIT_DATE@"; const std::string Version::GIT_COMMIT_SUBJECT = "@GIT_COMMIT_SUBJECT@"; 

version.hh

 #pragma once #include <string> namespace my_app { struct Version { static const std::string GIT_SHA1; static const std::string GIT_DATE; static const std::string GIT_COMMIT_SUBJECT; }; } 

然后在代码中,我可以写:

 cout << "Build SHA1: " << Version::GIT_SHA1 << endl; 

下面的解决scheme是基于观察Git更新HEAD日志,每当你pullcommit东西。 请注意,例如,如果您在每次commit后手动重buildCMakecaching,Drew的build议将会更新Git信息。

我使用CMake“自定义命令”来生成单行头文件${SRCDIR}/gitrevision.hh ,其中${SRCDIR}是源代码树的根目录。 只有在提交新的提交时才会重新制作。 这里有一些必要的CMake魔法:

 # Generate gitrevision.hh if Git is available # and the .git directory is present # this is the case when the software is checked out from a Git repo find_program(GIT_SCM git DOC "Git version control") mark_as_advanced(GIT_SCM) find_file(GITDIR NAMES .git PATHS ${CMAKE_SOURCE_DIR} NO_DEFAULT_PATH) if (GIT_SCM AND GITDIR) # Create gitrevision.hh # that depends on the Git HEAD log add_custom_command(OUTPUT ${SRCDIR}/gitrevision.hh COMMAND ${CMAKE_COMMAND} -E echo_append "#define GITREVISION " > ${SRCDIR}/gitrevision.hh COMMAND ${GIT_SCM} log -1 "--pretty=format:%h %ai" >> ${SRCDIR}/gitrevision.hh DEPENDS ${GITDIR}/logs/HEAD VERBATIM ) else() # No version control # eg when the software is built from a source tarball # and gitrevision.hh is packaged with it but no Git is available message(STATUS "Will not remake ${SRCDIR}/gitrevision.hh") endif() 

gitrevision.hh的内容如下所示:

 #define GITREVISION cb93d53 2014-03-13 11:08:15 +0100 

如果你想改变这个,然后相应地编辑--pretty=format:规范。 例如使用%H而不是%h将打印完整的SHA1摘要。 有关详细信息,请参阅Git手册。

使gitrevision.hh成为一个完全成熟的C + +头文件与包括守卫等留给练习读者:-)

有一个解决scheme可以捕获存储库的变化(来自git describe --dirty ),但是如果有关于git信息的变化只会触发重新编译。

一些现有的解决scheme:

  1. 使用“execute_process”。 这只会在configuration时获得git信息,并且可能会错过对版本库的更改。
  2. 取决于.git/logs/HEAD 。 这只会在repo中的某些内容发生变化时触发重新编译,但会错过更改以获取“-dirty”状态。
  3. 每次构build运行时,使用自定义命令来重build版本信息。 这会捕获导致-dirty状态的更改,但会-dirty触发重新编译(基于版本信息文件的更新时间戳)

对第三种解决scheme的一个解决方法是使用CMake'copy_if_different'命令,因此版本信息文件上的时间戳仅在内容更改时才会更改。

自定义命令中的步骤是:

  1. 将git信息收集到一个临时文件
  2. 使用“copy_if_different”将临时文件复制到实际文件
  3. 删除临时文件,触发自定义命令再次运行下一个“make”

代码(大量借鉴kralyk的解决scheme):

 # The 'real' git information file SET(GITREV_BARE_FILE git-rev.h) # The temporary git information file SET(GITREV_BARE_TMP git-rev-tmp.h) SET(GITREV_FILE ${CMAKE_BINARY_DIR}/${GITREV_BARE_FILE}) SET(GITREV_TMP ${CMAKE_BINARY_DIR}/${GITREV_BARE_TMP}) ADD_CUSTOM_COMMAND( OUTPUT ${GITREV_TMP} COMMAND ${CMAKE_COMMAND} -E echo_append "#define GIT_BRANCH_RAW " > ${GITREV_TMP} COMMAND ${GIT_EXECUTABLE} rev-parse --abbrev-ref HEAD >> ${GITREV_TMP} COMMAND ${CMAKE_COMMAND} -E echo_append "#define GIT_HASH_RAW " >> ${GITREV_TMP} COMMAND ${GIT_EXECUTABLE} describe --always --dirty --abbrev=40 --match="NoTagWithThisName" >> ${GITREV_TMP} COMMAND ${CMAKE_COMMAND} -E copy_if_different ${GITREV_TMP} ${GITREV_FILE} COMMAND ${CMAKE_COMMAND} -E remove ${GITREV_TMP} WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} VERBATIM ) # Finally, the temporary file should be added as a dependency to the target ADD_EXECUTABLE(test source.cpp ${GITREV_TMP}) 

我不能帮助你在CMake方面,但对于Git方面,我会build议看看Linux内核和Git项目本身如何做,通过GIT-VERSION-GEN脚本,或者如何在它的Makefile中 ,通过使用git describe是否存在git存储库,回退到“ version ”/“ VERSION ”/“ GIT-VERSION-FILE ”生成并存在于tarball中,最后回落到脚本(或Makefile)中硬编码的默认值。

第一部分(使用git describe )要求使用带注释的(可能是GPG签名)标签来标记发布。 或者使用git describe --tags来使用轻量级标签。

这是我的解决scheme,我认为它是相当短而有效的;-)

首先,在源代码树中需要一个文件(我把它命名为git-rev.h.in ),它应该看起来像这样:

 #define STR_EXPAND(x) #x #define STR(x) STR_EXPAND(x) #define GIT_REV STR(GIT_REV_) #define GIT_REV_ \ 

(请不要介意这些macros,这是一个有点疯狂的技巧,使一个string从一个原始值。)这是至关重要的,这个文件有一个空的换行符,所以值可以追加。

现在这个代码进入相应的CMakeLists.txt文件:

 # --- Git revision --- add_dependencies(your_awesome_target gitrev) #put name of your target here include_directories(${CMAKE_CURRENT_BINARY_DIR}) #so that the include file is found set(gitrev_in git-rev.h.in) #just filenames, feel free to change them... set(gitrev git-rev.h) add_custom_target(gitrev ${CMAKE_COMMAND} -E remove -f ${CMAKE_CURRENT_BINARY_DIR}/${gitrev} COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/${gitrev_in} ${CMAKE_CURRENT_BINARY_DIR}/${gitrev} COMMAND git rev-parse HEAD >> ${CMAKE_CURRENT_BINARY_DIR}/${gitrev} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} #very important, otherwise git repo might not be found in shadow build VERBATIM #portability wanted ) 

这个命令确保git-rev.h.in被复制到构build树中,作为git-rev.h和git修订版本被添加到结尾。

所以你下一步需要做的就是在你的一个文件中包含git-rev.h ,并用GIT_REVmacros做任何你想做的GIT_REV ,这会产生当前的git修订哈希值作为string值。

这个解决schemegit-rev.h在于,每次构build相关目标时都会重新创buildgit-rev.h ,所以不必一次又一次地运行cmake

它也应该是相当便携 – 没有不可移植的外部工具,甚至血腥愚蠢的Windows CMD支持>>>操作符;-)

只需添加一些代码只有2个文件: CMakeList.txtmain.cpp

1. CMakeList.txt

 # git commit hash macro execute_process( COMMAND git log -1 --format=%h WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} OUTPUT_VARIABLE GIT_COMMIT_HASH OUTPUT_STRIP_TRAILING_WHITESPACE ) add_definitions("-DGIT_COMMIT_HASH=\"${GIT_COMMIT_HASH}\"") 

2. main.cpp

 inline void LogGitCommitHash() { #ifndef GIT_COMMIT_HASH #define GIT_COMMIT_HASH "0000000" // 0000000 means uninitialized #endif std::cout << "GIT_COMMIT_HASH[" << GIT_COMMIT_HASH << "]"; // 4f34ee8 } 

说明

CMakeList.txt ,CMake命令execute_process()用于调用命令git log -1 --format=%h ,它给出了string中SHA-1值的简短和唯一缩写,如4f34ee8 。 这个string被分配给名为GIT_COMMIT_HASH CMakevariables。 在gcc编译之前,CMake命令add_definitions()将macrosGIT_COMMIT_HASH定义为GIT_COMMIT_HASH的值。 散列值被预处理器用来代替C ++代码中的macros,因此存在于目标文件main.o和已编译的二进制文件a.out

边注

另一种实现方法是使用名为configure_file() CMake命令,但是我不喜欢使用它,因为在运行CMake之前文件不存在。

如果CMake没有内置的function来做这个replace,那么你可以编写一个包装shell脚本来读取一个模板文件,用正确的位置代替上面的SHA1哈希(例如使用sed ),创build真正的CMake构build文件,然后调用CMake来构build您的项目。

稍微不同的方法可能是使SHA1replace可选 。 您将创build一个虚拟散列值,如"NO_OFFICIAL_SHA1_HASH"的CMake文件。 当开发人员从其工作目录构build自己的构build版时,构build的代码将不包含SHA1哈希值(仅为虚拟值),因为工作目录中的代码甚至没有相应的SHA1哈希值。

另一方面,当您的构build服务器通过从中央存储库中提取的源文件进行官方构build时,您将知道源代码的SHA1哈希值。 此时,您可以用CMake文件中的散列值replace,然后运行CMake。

对于使用CMake将git SHA-1转化为C或C ++项目的快速而肮脏,可能不可移植的方法,我在CMakeLists.txt中使用了这个:

 add_custom_target(git_revision.h git log -1 "--format=format:#define GIT_REVISION \"%H\"%n" HEAD > git_revision.h WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} VERBATIM) 

它假定CMAKE_SOURCE_DIR是git存储库的一部分,并且该git在系统上可用,并且输出redirect将由shell正确parsing。

然后你可以使这个目标成为任何其他目标的依赖

 add_dependencies(your_program git_revision.h) 

每次构buildyour_program ,Makefile(或其他构build系统,如果在其他构build系统上工作)将在源目录中重新创buildgit_revision.h,其内容

 #define GIT_REVISION "<SHA-1 of the current git revision>" 

所以你可以从一些源代码文件#include git_revision.h并以这种方式使用它。 请注意,头文件是在字面上每个构build中创build的,即使其他每个对象文件都是最新的,它仍将运行此命令来重新创buildgit_revision.h。 我认为这不应该是一个巨大的问题,因为通常你不会重复一遍又一遍地重build相同的git修订版本,但这是要注意的事情,如果这对你一个问题,那么不要使用它。 (使用add_custom_command可能会解决一个解决方法,但到目前为止我并不需要它。)