简体   繁体   English

如何使 Git 提交哈希在 C++ 代码中可用而无需不必要的重新编译?

[英]How to make Git commit hash available in C++ code without needless recompiling?

A fairly common requirement, methinks: I want myapp --version to show the version and the Git commit hash (including whether the repository was dirty).我认为这是一个相当普遍的要求:我希望myapp --version显示版本和 Git 提交哈希(包括存储库是否脏)。 The application is being built through a Makefile (actually generated by qmake , but let's keep it "simple" for now).该应用程序是通过Makefile构建的(实际上是由qmake生成的,但现在让我们保持“简单”)。 I'm fairly well versed in Makefiles, but this one has me stumped.我对 Makefile 相当精通,但是这个让我很难过。

I can easily get the desired output like this :我可以像这样轻松获得所需的输出:

$ git describe --always --dirty --match 'NOT A TAG'
e0e8556-dirty

The C++ code expects the commit hash to be made available as a preprocessor macro named GIT_COMMIT , eg: C++ 代码期望提交哈希作为名为GIT_COMMIT的预处理器宏GIT_COMMIT ,例如:

#define GIT_COMMIT "e0e8556-dirty" // in an include file
-DGIT_COMMIT=e0e8556-dirty         // on the g++ command line

Below are the several different ways I have tried to plumb the git describe output through to C++.下面是我尝试将git describe输出连接到 C++ 的几种不同方法。 None of them work perfectly.他们都没有完美地工作。

Approach The First: the $(shell) function.方法一: $(shell)函数。

We use make's $(shell) function to run the shell command and stick the result into a make variable:我们使用 make 的$(shell)函数来运行 shell 命令并将结果粘贴到 make 变量中:

GIT_COMMIT := $(shell git describe --always --dirty --match 'NOT A TAG')

main.o: main.cpp
    g++ -c -DGIT_COMMIT=$(GIT_COMMIT) -o$@ $<

This works for a clean build, but has a problem: if I change the Git hash (eg by committing, or modifying some files in a clean working copy), these changes are not seen by make, and the binary does not get rebuilt.这适用于干净的构建,但有一个问题:如果我更改 Git 哈希(例如,通过提交或修改干净工作副本中的某些文件),make 看不到这些更改,并且二进制文件不会重新构建。

Approach The Second: generating version.h方法二:生成version.h

Here, we use a make recipe to generate a version.h file containing the necessary preprocessor defines.在这里,我们使用 make recipe 生成一个version.h文件,其中包含必要的预处理器定义。 The target is phony so that it always gets rebuilt (otherwise, it would always be seen as up to date after the first build).目标是虚假的,因此它总是会被重建(否则,它总是会在第一次构建后被视为最新的)。

.PHONY: version.h
version.h:
    echo "#define GIT_COMMIT \"$(git describe --always --dirty --match 'NOT A TAG')\"" > $@

main.o: main.cpp version.h
    g++ -c -o$@ $<

This works reliably and does not miss any changes to the Git commit hash, but the problem here is that it always rebuilds version.h and everything that depends on it (including a fairly lengthy link stage).这工作可靠并且不会错过对 Git 提交哈希的任何更改,但这里的问题是它总是重建version.h和所有依赖它的东西(包括相当长的链接阶段)。

Approach The Third: only generating version.h if it has changed方法之三:仅在version.h发生变化时生成它

The idea: if I write the output to version.h.tmp , and then compare this to the existing version.h and only overwrite the latter if it's different, we wouldn't always need to rebuild.想法:如果我将输出写入version.h.tmp ,然后将其与现有的version.h进行比较,并且仅在后者不同时才覆盖后者,则我们并不总是需要重建。

However, make figures out what it needs to rebuild before actually starting to run any recipes.但是,在实际开始运行任何配方之前,先弄清楚它需要重建什么。 So this would have to come before that stage, ie also run from a $(shell) function.所以这必须在那个阶段之前出现,即也从$(shell)函数运行。

Here's my attempt at that:这是我的尝试:

$(shell echo "#define GIT_COMMIT \"$$(git describe --always --dirty --match 'NOT A TAG')\"" > version.h.tmp; if diff -q version.h.tmp version.h >/dev/null 2>&1; then rm version.h.tmp; else mv version.h.tmp version.h; fi)

main.o: main.cpp version.h
    g++ -c -o$@ $<

This almost works: whenever the Git hash changes, the first build regenerates version.h and recompiles, but so does the second build.几乎有效:每当 Git 哈希更改时,第一个构建会重新生成version.h并重新编译,但第二个构建也是如此。 From then on, make decides that everything is up to date.从那时起,make 决定一切都是最新的。

So it would seem that make decides what to rebuild even before it runs the $(shell) function, which renders this approach broken as well.所以看起来 make 甚至在它运行$(shell)函数之前就决定了要重建什么,这也使这种方法被破坏了。

This seems like such a common thing, and with make being such a flexible tool, I find it hard to believe that there is no way to get this 100% right.这似乎是一件很常见的事情,而且 make 是一个如此灵活的工具,我发现很难相信没有办法做到 100% 正确。 Does such an approach exist?这种方法存在吗?

I found a nice solution here :我在这里找到了一个很好的解决方案:

In your CMakeLists.txt put:在你的CMakeLists.txt

# Get the current working branch
execute_process(
    COMMAND git rev-parse --abbrev-ref HEAD
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
    OUTPUT_VARIABLE GIT_BRANCH
    OUTPUT_STRIP_TRAILING_WHITESPACE)

# Get the latest commit hash
execute_process(
    COMMAND git rev-parse HEAD
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
    OUTPUT_VARIABLE GIT_COMMIT_HASH
    OUTPUT_STRIP_TRAILING_WHITESPACE)

and then define it in your source:然后在你的源代码中定义它:

target_compile_definitions(${PROJECT_NAME} PRIVATE
    "-DGIT_COMMIT_HASH=\"${GIT_COMMIT_HASH}\"")

In the source it will now be available as a #define .在源代码中,它现在可以作为#define One might want to make sure that the source still compiles correctly by including :人们可能希望通过包含以下内容来确保源代码仍然正确编译:

#ifndef GIT_COMMIT_HASH
#define GIT_COMMIT_HASH "?"
#endif

Then you are ready to use, with for example:然后您就可以使用了,例如:

std::string hash = GIT_COMMIT_HASH;

It turns out my third approach was fine after all: $(shell) does run before make figures out what to rebuild.事实证明,我的第三种方法毕竟很好: $(shell)确实在 make 确定要重建的内容之前运行。 The problem was that, during my isolated tests, I accidentally committed version.h to the repository, which caused the double rebuild.问题是,在我的隔离测试期间,我不小心将version.h提交到存储库,这导致了双重重建。

But there is room for improvement still, thanks to @BasileStarynkevitch and @RenaudPacalet: if version.h is used from multiple files, it's nicer to store the hash in a version.cpp file instead, so we only need to recompile one tiny file and re-link.但是仍有改进的空间,感谢@BasileStarynkevitch 和@RenaudPacalet:如果从多个文件使用version.h ,最好将哈希存储在version.cpp文件中,因此我们只需要重新编译一个小文件,然后重新链接。

So here's the final solution:所以这是最终的解决方案:

version.h版本.h

#ifndef VERSION_H
#define VERSION_H
extern char const *const GIT_COMMIT;
#endif

Makefile生成文件

$(shell echo -e "#include \"version.h\"\n\nchar const *const GIT_COMMIT = \"$$(git describe --always --dirty --match 'NOT A TAG')\";" > version.cpp.tmp; if diff -q version.cpp.tmp version.cpp >/dev/null 2>&1; then rm version.cpp.tmp; else mv version.cpp.tmp version.cpp; fi)

# Normally generated by CMake, qmake, ...
main: main.o version.o
    g++ -o$< $?
main.o: main.cpp version.h
    g++ -c -o$@ $<
version.o: version.cpp version.h
    g++ -c -o$@ $<

Thanks everyone for chiming in with alternatives!感谢大家提出替代方案!

First of all, you could generate a phony version.h but use it only in version.cpp that defines the print_version function used everywhere else.首先,您可以生成一个虚假的version.h但只能在version.cpp中使用它,该version.cpp定义了在其他地方使用的print_version函数。 Each invocation of make while nothing changed would then cost you only one ultra-fast compilation of version.cpp plus the fairly lengthy link stage .每次调用 make 而没有任何变化,您只需花费一次超快的version.cpp编译加上相当长的链接阶段 No other re-compilations.没有其他重新编译。

Next, you can probably solve your problem with a bit of recursive make:接下来,您可能可以通过一些递归 make 来解决您的问题:

TARGETS := $(patsubst %.cpp,%.o,$(wildcard *.cpp)) ...

ifeq ($(MODE),)
$(TARGETS): version
    $(MAKE) MODE=1 $@

.PHONY: version

version:
    VERSION=$$(git describe --always --dirty) && \
    printf '#define GIT_COMMIT "%s"\n' "$$VERSION" > version.tmp && \
    if [ ! -f version.h ] || ! diff --brief version.tmp version.h &> /dev/null; then \
        cp version.tmp version.h; \
    fi
else
main.o: main.cpp version.h
    g++ -c -o$@ $<

...
endif

The $(MAKE) MODE=1 $@ invocation will do something if and only if version.h has been modified by the first make invocation (or if the target had to be re-built anyway). $(MAKE) MODE=1 $@调用将在且仅当第一次 make 调用修改了version.h时(或者无论如何必须重新构建目标)才会执行某些操作。 And the first make invocation will modify version.h if and only if the commit hash changed.当且仅当提交哈希更改时,第一次 make 调用将修改version.h

Using .PHONY directly means the target file is presumed not to exist, which you don't want for real files.直接使用.PHONY意味着假定目标文件不存在,而您不希望它用于真实文件。 To force a recipe that might rebuild a file, make it depend on a phony target.要强制可能重建文件的配方,请使其依赖于虚假目标。 Like so:像这样:

.PHONY: force
version.c: force
        printf '"%s"' `git describe --always --dirty` | grep -qsf - version.c \
        || printf >version.c 'const char version[]="%s";\n' `git describe --always --dirty`

(except markdown doesn't understand tabs, you have to fix that in the paste) (除了降价不理解标签,你必须在粘贴中修复它)

and the version.c recipe will run every time, since its phony dependency is presumed not to exist, but things that depend on version.c will check the real file, which only really gets updated if its contents didn't have the current version.并且version.c配方每次都会运行,因为它的虚假依赖被假定不存在,但是依赖 version.c 的东西将检查真实文件,只有在其内容没有当前版本时才会真正更新.

Or you could generate the version string in version.h as with the "Approach the Second" setup in your question, the important thing is not to tell make real files are phony.或者您可以在version.h生成版本字符串,就像您问题中的“接近第二个”设置一样,重要的是不要告诉make真实文件是假的。

Why not have version.h depend on your .git/index file?为什么不让version.h依赖于你的.git/index文件? That is touched whenever you commit or change something in your staging area (which does not happen often, usually).每当您在临时区域提交或更改某些内容时(通常不会经常发生),都会触及这一点。

version.h: .git/index
    echo "#define GIT_COMMIT \"$(git describe --always --dirty)\"" > $@

If you plan on building without Git at some point, you will need to change this, of course...如果您计划在某个时候不使用 Git 进行构建,那么您当然需要更改此设置...

I suggest generating a tiny self-sufficient C file version.c defining some global variables, and ensuring it is regenerated at every successful link of myapp executable.我建议生成一个小的自给自足的 C 文件version.c定义一些全局变量,并确保在myapp可执行文件的每个成功链接处重新生成它。

So in your makefile所以在你的makefile中

 version.c:
       echo "const char version_git_commit[]=" > $@
       echo "  \"$(git describe --always --dirty)\";" >> $@

Then have some C++ header declaring it:然后有一些 C++ 头文件声明它:

extern "C" const char version_git_commit[];

BTW, look into my bismon repository (commit c032c37be992a29a1e ), its Makefile , target file __timestamp.c for inspiration.顺便说一句,查看我的bismon存储库(提交c032c37be992a29a1e ),它的Makefile ,目标文件__timestamp.c以获得灵感。 Notice that for the binary executable bismonion target, make is removing __timestamp.c after each successful link.请注意,对于二进制可执行文件bismonion目标, make会在每次成功链接后删除__timestamp.c

You could improve your Makefile to remove version.c and version.o after each successful executable linking (eg after some $(LINK.cc) line for your myapp executable).您可以改进您的Makefile以在每次成功链接可执行文件后删除version.cversion.o (例如,在myapp可执行文件的某些$(LINK.cc)行之后)。 Hence you would have in your makefile:因此,您的 makefile 中将包含:

myapp: #list of dependencies, with version.o ....
      $(LINK.cc) .... version.o ... -o $@
      $(RM) version.o version.c

So you could have only your version.c and version.o rebuilt every time, and that is very quick.因此,您每次只能重建version.cversion.o ,而且速度非常快。

You can get it by calling git rev-parse --short HEAD command directly from your executable您可以通过直接从可执行文件调用git rev-parse --short HEAD命令来获取它

Here is what I did:这是我所做的:

in CMakeLists.txt在 CMakeLists.txt 中

add_definitions("-DPROJECT_DIR=\"${CMAKE_CURRENT_SOURCE_DIR}\"")

and in your source file:并在您的源文件中:


#include <array>
#include <cstdio>
#include <iostream>
#include <memory>
#include <stdexcept>
#include <string>

inline std::string execCommand(const char* cmd) {
  std::array<char, 128> buffer;
  std::string result;
  std::unique_ptr<FILE, decltype(&pclose)> pipe(popen(cmd, "r"), pclose);
  if (!pipe) {
    throw std::runtime_error("popen() failed!");
  }
  while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr) {
    result += buffer.data();
  }
  return result;
}


int main()
{
  std::string git_command = "cd ";
  git_command += PROJECT_DIR;  // get your project directory from cmake variable
  git_command += " && git rev-parse --short HEAD";  // get the git commit id in your project directory

  std::cout << "Git commit id is :" << execCommand(git_command.c_str()) << std::endl;

  return 0;
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM