Table of Contents

2024-11-25 - proper version.hpp with cmake

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:

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 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 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:

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. ;)