Reputation: 181835
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.
$(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.
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).
version.h
if it has changedThe 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?
Upvotes: 21
Views: 12877
Reputation: 5965
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;
Upvotes: 14
Reputation: 508
You can get it by calling git rev-parse --short HEAD
command directly from your executable
Here is what I did:
in 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;
}
Upvotes: -2
Reputation: 181835
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!
Upvotes: 7
Reputation: 60323
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.
Upvotes: 2
Reputation: 1
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.
So in your makefile
version.c:
echo "const char version_git_commit[]=" > $@
echo " \"$(git describe --always --dirty)\";" >> $@
Then have some C++ header declaring it:
extern "C" const char version_git_commit[];
BTW, look into my bismon repository (commit c032c37be992a29a1e), its Makefile
, target file __timestamp.c
for inspiration. Notice that for the binary executable bismonion target, make
is removing __timestamp.c
after each successful link.
You could improve your Makefile
to remove version.c
and version.o
after each successful executable linking (e.g. after some $(LINK.cc)
line for your myapp
executable). Hence you would have in your 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.
Upvotes: 0
Reputation: 29230
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.
Upvotes: 3
Reputation: 30860
Why not have version.h
depend on your .git/index
file? 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...
Upvotes: 1