====== 2024-11-25 - proper version.hpp with cmake ======
while working on [[https://github.com/el-bart/yals|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) changes
i 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 [[wp>reproducible builds]].
===== the cmake way =====
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 [[wp>CMake|cmake]]'s build-in [[https://cmake.org/cmake/help/latest/command/configure_file.html|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)
* cmake will regenerate this file only when it does not exist
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 size and build times =====
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. :)
===== ensuring version.cpp is up to date =====
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.
===== final thoughts =====
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. ;)