How to make Git commit hash available in C++ code without needless recompiling?
I found a nice solution here:
In your CMakeLists.txt
put:
# 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
. 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;
Using .PHONY
directly means the target file is presumed not to exist, which you don't want for real files. 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.
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.
It turns out my third approach was fine after all: $(shell)
does run before make figures out what to rebuild. The problem was that, during my isolated tests, I accidentally committed version.h
to the repository, which caused the double rebuild.
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.
So here's the final solution:
version.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. 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. No other re-compilations.
Next, you can probably solve your problem with a bit of recursive 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). And the first make invocation will modify version.h
if and only if the commit hash changed.