A fairly common requirement, methinks: I want myapp --version
to show the version and the Git commit hash (including whether the repository was dirty). The application is being built through a Makefile
(actually generated by qmake
, but let's keep it "simple" for now). I'm fairly well versed in Makefiles, but this one has me stumped.
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
, e.g.:
#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++. None of them work perfectly.
Approach The First: the $(shell)
function.
We use make's $(shell)
function to run the shell command and stick the result into a make variable:
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 (e.g. 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.
Approach The Second: generating version.h
Here, we use a make recipe to generate a version.h
file containing the necessary preprocessor defines. 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).
Approach The Third: only generating version.h
if it has changed
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.
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, i.e. also run from a $(shell)
function.
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. From then on, make decides that everything is up to date.
So it would seem that make decides what to rebuild even before it runs the $(shell)
function, which renders this approach broken as well.
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. Does such an approach exist?