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 [email protected] $<
version.o: version.cpp version.h
    g++ -c [email protected] $<

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 [email protected]

.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 [email protected] $<

...
endif

The $(MAKE) MODE=1 [email protected] 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.