在 Makefile 中,这将通过以下方式完成:
g++ -DGIT_SHA1="`git log -1 | head -n 1`" ...
这非常有用,因为二进制文件知道确切的提交 SHA1,因此它可以在出现段错误时转储它。
如何使用 CMake 实现相同的目标?
我已经制作了一些 CMake 模块,这些模块可以与 git repo 进行版本控制和类似目的 - 它们都在我的存储库中https://github.com/rpavlik/cmake-modules
这些函数的好处是,每次 HEAD 提交更改时,它们都会在构建之前强制重新配置(重新运行 cmake)。与使用 execute_process 只做一次不同的是,您不需要记住重新 cmake 来更新散列定义。
为此,您至少需要GetGitRevisionDescription.cmake
和GetGitRevisionDescription.cmake.in
文件。然后,在你的主CMakeLists.txt
文件中,你会有这样的东西
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/whereYouPutMyModules/")
include(GetGitRevisionDescription)
get_git_head_revision(GIT_REFSPEC GIT_SHA1)
然后,您可以将其添加为系统范围的定义(不幸的是,这会导致大量重建)
add_definitions("-DGIT_SHA1=${GIT_SHA1}")
或者,我建议的替代方案:制作生成的源文件。在您的源代码中创建这两个文件:
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
(假设您在 SOURCES 中有源文件列表):
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)
然后,您有一个包含您的 SHA 字符串的全局变量 - 当 SHA 发生变化时,带有 extern 的标头不会更改,因此您可以只包含您想要引用字符串的任何位置,然后只需要生成的 CPP在每次提交时重新编译,以便您可以在任何地方访问 SHA。
我这样做是为了生成:
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";
如果执行构建的工作区有待处理的、未提交的更改,则上述 SHA1 字符串将以-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;
如果有一个解决方案可以捕获对存储库的更改(来自git describe --dirty
),但只有在有关 git 信息的某些内容发生更改时才会触发重新编译。
一些现有的解决方案:
execute_process
. 这只会在配置时获取 git 信息,并且可能会错过对存储库的更改。.git/logs/HEAD
. 这只会在 repo 中的某些内容发生更改时触发重新编译,但会错过获取-dirty
状态的更改。-dirty
状态的更改,但会一直触发重新编译(基于版本信息文件的更新时间戳)第三种解决方案的一个修复方法是使用 CMakecopy_if_different
命令,因此版本信息文件上的时间戳仅在内容更改时才会更改。
自定义命令中的步骤是:
copy_if_different
将临时文件复制到真实文件make
代码(大量借鉴 kralyk 的解决方案):
# 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})
我会在我的 CMakeLists.txt 中使用类似的东西:
execute_process(
COMMAND git describe
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
OUTPUT_VARIABLE VERSION )
string( REGEX MATCH "-g.*$" VERSION_SHA1 ${VERSION} )
string( REGEX REPLACE "[-g]" "" VERSION_SHA1 ${VERSION_SHA1} )
add_definitions( -DGIT_SHA1="${VERSION_SHA1}" )
以下解决方案是基于观察到 Git 会在您pull
或commit
某事时更新 HEAD 日志。请注意,仅当您在每个commit
.
我使用 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
# e.g. 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++ 头文件作为练习留给读者:-)
只需将一些代码添加到仅 2 个文件中:CMakeList.txt
和main.cpp
.
# 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}\"")
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
,该命令为您的 SHA-1 值提供简短且唯一的缩写,如4f34ee8
. 该字符串被分配给名为 的 CMake 变量GIT_COMMIT_HASH
。CMake 命令add_definitions()
将宏定义为gcc 编译之前GIT_COMMIT_HASH
的值。4f34ee8
哈希值用于通过预处理器替换 C++ 代码中的宏,因此存在于目标文件main.o
和编译后的二进制文件中a.out
。
另一种实现方法是使用名为 的 CMake 命令configure_file()
,但我不喜欢使用它,因为在运行 CMake 之前该文件不存在。
这是我的解决方案,我认为它相当短但有效;-)
首先,在源代码树中需要一个文件(我命名它git-rev.h.in
),它应该看起来像这样:
#define STR_EXPAND(x) #x
#define STR(x) STR_EXPAND(x)
#define GIT_REV STR(GIT_REV_)
#define GIT_REV_ \
(请不要介意那些宏,用原始值制作字符串有点疯狂。)这个文件最后必须有一个空换行符,以便可以附加值。
现在这段代码进入各自的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
复制到构建树中,git-rev.h
并在其末尾附加 git 修订。
所以接下来你需要做的就是包含git-rev.h
在你的一个文件中,并使用宏做任何你想做的事情GIT_REV
,这会产生当前的 git 修订哈希作为字符串值。
这个解决方案的git-rev.h
好处是每次构建关联目标时都会重新创建,因此您不必cmake
一遍又一遍地运行。
它也应该非常便携——没有使用非便携的外部工具,甚至该死的愚蠢的 Windows cmd 也支持>
and>>
运算符;-)
我无法在 CMake 方面为您提供帮助,但关于Git 方面,我建议您通过GIT-VERSION-GEN脚本查看 Linux 内核和 Git 项目本身是如何做到的,或者 tig 如何在其Makefile中做到这一点,通过使用git describe
是否存在 git 存储库,回退到生成并存在于 tarball 中的“ version
”/“ VERSION
”/“ GIT-VERSION-FILE
”,最后回退到脚本(或 Makefile)中硬编码的默认值。
第一部分(使用git describe
)要求您使用带注释的(可能是 GPG 签名的)标签来标记发布。或者git describe --tags
也可以使用轻量级标签。
如果 CMake 没有内置功能来执行此替换,那么您可以编写一个读取模板文件的包装器 shell 脚本,在正确的位置替换上面的 SHA1 哈希(sed
例如,使用 ),创建真正的CMake 构建文件,然后调用 CMake 来构建您的项目。
稍微不同的方法可能是使 SHA1 替换可选。您将创建具有虚拟哈希值的 CMake 文件,例如"NO_OFFICIAL_SHA1_HASH"
. 当开发人员从他们的工作目录构建他们自己的构建时,构建的代码不会包含 SHA1 哈希值(只有虚拟值),因为工作目录中的代码甚至还没有相应的 SHA1 哈希值。
另一方面,当您的构建服务器从中央存储库中提取的源进行正式构建时,您就会知道源代码的 SHA1 哈希值。此时,您可以替换 CMake 文件中的哈希值,然后运行 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 在系统上可用,并且输出重定向将由 shell 正确解析。
然后,您可以使用此目标使该目标成为任何其他目标的依赖项
add_dependencies(your_program git_revision.h)
每次your_program
构建时,Makefile(或其他构建系统,如果这适用于其他构建系统)将在源目录中重新创建 git_revision.h,其内容
#define GIT_REVISION "<SHA-1 of the current git revision>"
所以你可以#include git_revision.h
从一些源代码文件中使用它。请注意,标头实际上是在每次构建时创建的,即即使每个其他目标文件都是最新的,它仍然会运行此命令来重新创建 git_revision.h。我认为这不应该是一个大问题,因为通常你不会一遍又一遍地重建相同的 git 版本,但这是需要注意的,如果这对你来说是个问题,那就不要使用它。(可能有可能破解一个解决方法,add_custom_command
但到目前为止我还不需要它。)