Compile-time git version info using CMake

Frank Vanbever -- Thursday, April 6, 2023

I recently found myself wanting to get some insight into what exactly went into a given binary that we deployed on an embedded system. As a first step I wanted to get the commit hash for the HEAD of the branch from which it was built.

After a bit of searching around I stumbled upon this blog post by Matthew Keeter which I took as a starting point. Instead of creating the file from the cmake script directly I instead opted to have a template file that would be filled in at configure time.

This is the modified cmake script:

 0  execute_process(COMMAND git log --pretty=format:'%h' -n 1
 1    OUTPUT_VARIABLE GIT_REV
 2    ERROR_QUIET
 3  )
 4
 5  if("${GIT_REV}" STREQUAL "")
 6    set(GIT_REV "N/A")
 7    set(GIT_DIFF "")
 8    set(GIT_TAG "N/A")
 9    set(GIT_BRANCH "N/A")
10  else()
11    execute_process(
12      COMMAND bash -c "git diff --quiet --exit-code || echo -dirty"
13      OUTPUT_VARIABLE GIT_DIFF)
14    execute_process(
15      COMMAND git describe --exact-match --tags OUTPUT_VARIABLE GIT_TAG ERROR_QUIET)
16    execute_process(
17      COMMAND git rev-parse --abbrev-ref HEAD OUTPUT_VARIABLE GIT_BRANCH)
18
19    string(STRIP "${GIT_REV}" GIT_REV)
20    string(SUBSTRING "${GIT_REV}" 1 7 GIT_REV)
21    string(STRIP "${GIT_DIFF}" GIT_DIFF)
22    string(STRIP "${GIT_TAG}" GIT_TAG)
23    string(STRIP "${GIT_BRANCH}" GIT_BRANCH)
24  endif()
25
26  configure_file(
27    "${SRC_DIR}/version.h.in"
28    "${BIN_DIR}/version.h"
29  )

We collect the following information:

  • GIT_REV is the current abbreviated commit hash
  • GIT_DIFF will contain the string -dirty if the tree from which it was built is dirty
  • GIT_TAG will contain the tag only if the current commit has a tag associated with it.
  • GIT_BRANCH will contain the name of the current branch

The template for the version header file is fairly straightforward.

0#ifndef VERSION_H
1#define VERSION_H
2
3#define GIT_REV "@GIT_REV@@GIT_DIFF@"
4#define GIT_TAG "@GIT_TAG@"
5#define GIT_BRANCH "@GIT_BRANCH@"
6
7#endif /* VERSION_H */

Triggering the generation of this file is in my opinion most easily accomplished by adding a target for it:

0  add_custom_target(gen-version-h
1    COMMAND "${CMAKE_COMMAND}"
2    "-D" "SRC_DIR=${PROJECT_SOURCE_DIR}/src"
3    "-D" "BIN_DIR=${CMAKE_CURRENT_BINARY_DIR}"
4    "-P" "${PROJECT_SOURCE_DIR}/support/version.cmake"
5    COMMENT "Generating git version file"
6  )
7
8  add_dependencies(my-application gen-version-h)
9  include_directories(${CMAKE_CURRENT_BINARY_DIR})

Because the new cmake process runs in a different context it does not have all the same variables defined that we have available while building the source code. To know where the source resides and where the configured file should be installed we need to pass in respective paths. This is what SRC_DIR and BIN_DIR accomplish.

Finally we need to make sure that the compiler will be able to find the file which we do by including the current binary directory in the search path.

The default behavior of this form of add_custom_target, without an output file, is that it will always be considered out of date, hence it'll be invoked every time we compile.

This way we get up-to-date information about the provenance of every build we make of the software.

I've created a simple demo application and put it on Github. It outputs the following when you compile it:

0GIT_REV is 62b4399
1GIT_TAG is foo
2GIT_BRANCH is main