如何在不进行不必要的重新编译的情况下使Git commit哈希在C ++代码中可用?

时间:2018-08-07 13:05:37

标签: c++ git makefile g++

一个相当常见的要求,方法是:我希望myapp --version显示版本和Git提交哈希(包括存储库是否脏)。该应用程序是通过Makefile构建的(实际上是由qmake生成的,但现在让它保持“简单”状态)。我对Makefiles相当熟悉,但是这一点让我感到困惑。

我可以轻松获得所需的输出like this

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

C ++代码期望提交哈希可以作为名为GIT_COMMIT的预处理器宏使用,例如:

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

以下是我尝试将git describe输出传递到C ++的几种不同方法。它们都不完美。

第一种方法:$(shell)函数。

我们使用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$@ $<

这对于干净的版本有效,但是有一个问题:如果我更改Git哈希(例如,通过提交或修改干净的工作副本中的某些文件),make不会看到这些更改,而二进制文件不会重建。

方法二:生成version.h

在这里,我们使用make配方来生成一个version.h文件,其中包含必要的预处理程序定义。目标是伪造的,因此总是可以重新构建(否则,在第一次构建之后,它总是会被视为最新的)。

.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$@ $<

这可靠地工作,并且不会丢失对Git commit哈希的任何更改,但是这里的问题是,它始终会重建version.h及其相关的所有内容(包括相当长的链接阶段)。

方法三:仅生成version.h(如果已更改)

这个想法:如果我将输出写入version.h.tmp,然后将其与现有的version.h进行比较,并且仅在后者不同时才覆盖后者,我们就不必总是重建。

但是,在实际开始运行任何配方之前,先弄清楚需要重建的内容。因此,这必须在该阶段之前完成,即也必须从$(shell)函数运行。

这是我的尝试:

$(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$@ $<

几乎起作用:每当Git哈希值更改时,第一个构建重新生成version.h并重新编译,而第二个构建也是如此。从那时起,make决定一切都是最新的。

因此,似乎make甚至在运行$(shell)函数之前就决定要重建什么,这也使这种方法无效。

这似乎是一件很平常的事情,并且由于使make成为一个如此灵活的工具,我很难相信没有办法使这100%正确。 是否存在这种方法?

7 个答案:

答案 0 :(得分:2)

直接使用.PHONY意味着假定目标文件不存在,而对于实际文件则不需要。要强制可能重新构建文件的配方,请使其依赖于伪造目标。像这样:

.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`

(除非markdown无法识别标签,否则您必须在粘贴中修复该问题)

version.c配方将每次运行,因为假定它的假性依赖关系不存在,但是依赖于version.c的事物将检查真实文件,只有在其内容不正确的情况下,该文件才会真正更新。没有最新版本。

或者您可以像在问题中的“第二次处理”设置一样在version.h中生成版本字符串,重要的是不要告诉make真实文件是假的。

答案 1 :(得分:1)

首先,您可以生成一个假音version.h,但只能在定义其他地方使用的version.cpp函数的print_version中使用它。每次调用make时,只要不做任何更改,都只会花费version.cpp的超快速编译加上相当长的链接阶段。没有其他重新编译。

接下来,您可以使用一些递归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

仅当$(MAKE) MODE=1 $@已被第一次make调用修改(或者无论如何还是必须重建目标)时,version.h调用才会执行某些操作。并且只有当提交哈希值更改时,第一个make调用才会修改version.h

答案 2 :(得分:1)

为什么services.AddIdentityServer取决于您的 String currentNetworkName = ""; ConnectivityManager connectivityManager = ((ConnectivityManager) context.getSystemService( Context.CONNECTIVITY_SERVICE)); NetworkInfo activeNetwork = connectivityManager.getActiveNetworkInfo(); boolean connected = activeNetwork != null && activeNetwork.isConnectedOrConnecting(); if (connected) { // prevent duplicate connect broadcasts String extraInfo = activeNetwork.getExtraInfo(); if(! currentNetworkName.equals(extraInfo)) { // to do: handle network changes currentNetworkName = extraInfo; } } else { Log.d(TAG, "is not connected"); isConnected = false; currentNetworkName = ""; } 文件?只要您在暂存区中提交或更改某些内容(通常不经常发生),便会触及到这一点。

version.h

如果您计划在某个时候不使用Git进行构建,那么您当然需要进行更改...

答案 3 :(得分:1)

事实证明,我的第三种方法毕竟还不错:$(shell) 确实在确定要重建的内容之前先运行。问题是,在隔离测试期间,我无意中将version.h提交给存储库,这导致了两次重建。

但是,由于@BasileStarynkevitch和@RenaudPacalet,仍有改进的空间:如果从多个文件中使用version.h,则最好将哈希存储在version.cpp文件中,所以我们只需要重新编译一个小文件并重新链接。

这是最终的解决方案:

version.h

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

制作文件

$(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$@ $<

感谢大家使用替代方法!

答案 4 :(得分:0)

我建议生成一个微小的自给自足的C文件version.c,该文件定义一些全局变量,并确保在myapp可执行文件的每个成功链接处都重新生成该文件。

因此在您的makefile中

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

然后有一些C ++标头声明它:

extern "C" const char version_git_commit[];

顺便说一句,请查看我的bismon存储库(提交c032c37be992a29a1e),其Makefile,目标文件__timestamp.c来激发灵感。请注意,对于二进制可执行文件bismonion,在每次成功链接后,make都会删除 __timestamp.c

您可以改进Makefile,以在每次成功链接可执行文件后删除version.cversion.o(例如,在$(LINK.cc)可执行文件的myapp行之后)。因此,您将在您的makefile中:

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

因此您每次只能重建version.cversion.o,而且速度很快。

答案 5 :(得分:0)

您可以通过直接从可执行文件中调用git rev-parse --short HEAD命令来获取它

这是我所做的:

在CMakeLists.txt中

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

以及您的源文件中:


#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;
}

答案 6 :(得分:0)

我找到了一个不错的解决方案here

在您的rsconf = { _id : "amsdb", members: [ { _id : 0, host : "database:34000"}, { _id : 1, host : "database2:34001" }, { _id : 2, host : "database3:34002" } ] } rs.initiate(rsconf); rs.conf(); 中放置:

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)

现在在源中,它可以作为target_compile_definitions(${PROJECT_NAME} PRIVATE "-DGIT_COMMIT_HASH=\"${GIT_COMMIT_HASH}\"") 来使用。可能需要通过 includes

来确保源仍然可以正确编译:
#define

然后您就可以使用了,例如:

#ifndef GIT_COMMIT_HASH
#define GIT_COMMIT_HASH "?"
#endif