while working on YALS i reached a point, where it would make sense to add some final touches. i wanted to generate version.hpp
header, that'd have all the stuff inside.
first things first – getting version info. IMHO the nicest way of doing this in git
is:
git describe --always --abbrev=10 --dirty
this can return stuff like:
v1.2
– i.e. this version is tagged as v1.2
v1.2-42-g5a61f67add
– i.e. 42 commits away from v1.2
, i.e. commit hash g5a61f67add
v1.2-42-g5a61f67add-dirty
– 42 commits away from v1.2
, i.e. commit hash g5a61f67add
, with some local (unversioned) changesi like this output – let's stick with it for this example. now it's time to integrate this with cmake.
btw: note that i deliberately only focus on version only and not time / date of build. the reason is to ensure reproducible builds.
the canonical way of doing it is to create a file like this:
// version.hpp #pragma once #define MY_SW_VERSION "@GIT_VERSION_INFO@"
and generate it with a cmake's build-in configure_file() function:
configure_file("${CMAKE_SOURCE_DIR}/version.hpp.in" "${CMAKE_BINARY_DIR}/version.hpp" @ONLY)
…and call it a day.
there are however issues with this approach:
version.hpp
is a header, thus string will effectively be copy-and-pasted all over the code (that could be an issue for small embedded system)version.hpp
is a header, this any change to its content will force major recompilation (these kinds of headers tend to be include in a lot of places)first 2 are major annoyances, but the last one is just horrible, as it means that the built-in version info may be outdated on incremental builds… which means you cannot really trust it… which defeats the whole purpose of putting this info into binary in a first place.
addressing first 2 issues is simple – instead of having variable definition in a header, just move this into a *.cpp
like this:
// version.hpp #pragma once char const* my_sw_version()
// version.cpp #include "version.hpp" char const* my_sw_version() { return "@GIT_VERSION_INFO@"; }
this way the string lives in only 1 object file. the only cmake different is that we now generate version.cpp
instead of version.hpp
… and that solves both issues at the same time. nice. :)
this one is tricky. one could write a rule such that it will regenerate version.cpp
each time, to make sure it's up to date. that would however cause re-link of all binaries every time we build. that's not acceptable.
there is however a trick in cmake one could use. first create a helper cmake file:
# version_proxy.cmake execute_process(COMMAND git describe --always --abbrev=10 --dirty WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" OUTPUT_VARIABLE GIT_VERSION_INFO OUTPUT_STRIP_TRAILING_WHITESPACE ) configure_file("${SRC}" "${DST}" @ONLY)
now inside the main CMakeLists.txt
call it out:
# CMakeLists.txt # ... add_custom_target(version_proxy "${CMAKE_COMMAND}" -D "SRC=${CMAKE_SOURCE_DIR}/version.cpp.in" -D "DST=${CMAKE_BINARY_DIR}/version.cpp" -P "${CMAKE_SOURCE_DIR}/version_proxy.cmake" SOURCE "${CMAKE_SOURCE_DIR}/version.cpp.in" BYPRODUCTS "${CMAKE_BINARY_DIR}/version.cpp" ) # ... add_executable(foo_bar main.cpp "${CMAKE_CURRENT_BINARY_DIR}/version.cpp") add_dependencies(foo_bar version_proxy) # ...
the trick is to add dependency on foo_bar
target, that reference version_proxy
custom target, that in turn calls cmake
explicitly, from command line, defining source, destination and what's expected as input and output files. this way cmake will always get called to re-evaluate the version_proxy.cmake
… that in turn does the generation the smart way – only if destination file would actually change.
this solution ticks all the boxes – it rebuilds version.cpp
every time when version string changes… and only when it changes. it does a minimal rebuild (1 cpp + linking of dependent binaries). it does not proliferate copies of strings all over the binary.
the interesting thing is that generating a version.hpp
is such an obvious feature, yet event today cmake
does not support it gracefully out of the box. the good news is that there is a reliable way of doing it, even if a bit verbose. ;)