How can I pass git SHA1 to compiler as definition using cmake?

GitCmakeSha1

Git Problem Overview


In a Makefile this would be done with something like:

g++ -DGIT_SHA1="`git log -1 | head -n 1`" ...

This is very useful, because the binary knows exact commit SHA1 so it can dump it in case of segfault.

How can I achieve the same with CMake?

Git Solutions


Solution 1 - Git

I've made some CMake modules that peer into a git repo for versioning and similar purposes - they're all in my repository at https://github.com/rpavlik/cmake-modules

The good thing about these functions is, they will force a re-configure (a rerun of cmake) before a build every time the HEAD commit changes. Unlike doing something just once with execute_process, you don't need to remember to re-cmake to update the hash definition.

For this specific purpose, you'd need at least the GetGitRevisionDescription.cmake and GetGitRevisionDescription.cmake.in files. Then, in your main CMakeLists.txt file, you'd have something like this

list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/whereYouPutMyModules/")
include(GetGitRevisionDescription)
get_git_head_revision(GIT_REFSPEC GIT_SHA1)

Then, you could either add it as a system-wide definition (which unfortunately would cause lots of rebuilding)

add_definitions("-DGIT_SHA1=${GIT_SHA1}")

or, my suggested alternative: Make a generated source file. Create these two files in your source:

GitSHA1.cpp.in:

#define GIT_SHA1 "@GIT_SHA1@"
const char g_GIT_SHA1[] = GIT_SHA1;

GitSHA1.h:

extern const char g_GIT_SHA1[];

Add this to your CMakeLists.txt (assuming you have a list of source files in SOURCES):

configure_file("${CMAKE_CURRENT_SOURCE_DIR}/GitSHA1.cpp.in" "${CMAKE_CURRENT_BINARY_DIR}/GitSHA1.cpp" @ONLY)
list(APPEND SOURCES "${CMAKE_CURRENT_BINARY_DIR}/GitSHA1.cpp" GitSHA1.h)

Then, you have a global variable containing your SHA string - the header with the extern doesn't change when the SHA does, so you can just include that any place you want to refer to the string, and then only the generated CPP needs to be recompiled on every commit to give you access to the SHA everywhere.

Solution 2 - Git

I did this in such as way as to generate:

const std::string Version::GIT_SHA1 = "e7fb69fb8ee93ac66f006406781138562d0250fb";
const std::string Version::GIT_DATE = "Thu Jan 9 14:17:56 2014";
const std::string Version::GIT_COMMIT_SUBJECT = "Fix all the bugs";

If the workspace that performed the build had pending, uncommitted changes, the above SHA1 string will be suffixed with -dirty.

In CMakeLists.txt:

# the commit's SHA1, and whether the building workspace was dirty or not
execute_process(COMMAND
  "${GIT_EXECUTABLE}" describe --match=NeVeRmAtCh --always --abbrev=40 --dirty
  WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
  OUTPUT_VARIABLE GIT_SHA1
  ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE)

# the date of the commit
execute_process(COMMAND
  "${GIT_EXECUTABLE}" log -1 --format=%ad --date=local
  WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
  OUTPUT_VARIABLE GIT_DATE
  ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE)

# the subject of the commit
execute_process(COMMAND
  "${GIT_EXECUTABLE}" log -1 --format=%s
  WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
  OUTPUT_VARIABLE GIT_COMMIT_SUBJECT
  ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE)

# generate version.cc
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/version.cc.in" "${CMAKE_CURRENT_BINARY_DIR}/version.cc" @ONLY)

list(APPEND SOURCES "${CMAKE_CURRENT_BINARY_DIR}/version.cc" version.hh)

This requires version.cc.in:

#include "version.hh"

using namespace my_app;

const std::string Version::GIT_SHA1 = "@GIT_SHA1@";
const std::string Version::GIT_DATE = "@GIT_DATE@";
const std::string Version::GIT_COMMIT_SUBJECT = "@GIT_COMMIT_SUBJECT@";

And version.hh:

#pragma once

#include <string>

namespace my_app
{
  struct Version
  {
    static const std::string GIT_SHA1;
    static const std::string GIT_DATE;
    static const std::string GIT_COMMIT_SUBJECT;
  };
}

Then in code I can write:

cout << "Build SHA1: " << Version::GIT_SHA1 << endl;

Solution 3 - Git

It would be nice to have a solution that catches changes to the repository (from git describe --dirty), but only triggers recompilation if something about the git information has changed.

Some of the existing solutions:

  1. Use execute_process. This only gets the git information at configure time, and can miss changes to the repository.
  2. Depend on .git/logs/HEAD. This only triggers recompilation when something in the repo changes, but misses the changes to get the -dirty state.
  3. Use a custom command to rebuild the version information every time a build is run. This catches changes resulting in the -dirty state, but triggers a recompilation all the time (based on the updated timestamp of the version information file)

One fix to the third solution is to use the CMake copy_if_different command, so the timestamp on the version information file only changes if the contents change.

The steps in the custom command are:

  1. Collect the git information to a temporary file
  2. Use copy_if_different to copy the temporary file to the real file
  3. Delete the temporary file, to trigger the custom command to run again on the next make

The code (borrowing heavily from kralyk's solution):

# The 'real' git information file
SET(GITREV_BARE_FILE git-rev.h)
# The temporary git information file
SET(GITREV_BARE_TMP git-rev-tmp.h)
SET(GITREV_FILE ${CMAKE_BINARY_DIR}/${GITREV_BARE_FILE})
SET(GITREV_TMP ${CMAKE_BINARY_DIR}/${GITREV_BARE_TMP})

ADD_CUSTOM_COMMAND(
  OUTPUT ${GITREV_TMP}
  COMMAND ${CMAKE_COMMAND} -E echo_append "#define GIT_BRANCH_RAW " > ${GITREV_TMP}
  COMMAND ${GIT_EXECUTABLE} rev-parse --abbrev-ref HEAD >> ${GITREV_TMP}
  COMMAND ${CMAKE_COMMAND} -E echo_append "#define GIT_HASH_RAW " >> ${GITREV_TMP}
  COMMAND ${GIT_EXECUTABLE} describe --always --dirty --abbrev=40 --match="NoTagWithThisName" >> ${GITREV_TMP}
  COMMAND ${CMAKE_COMMAND} -E copy_if_different ${GITREV_TMP} ${GITREV_FILE}
  COMMAND ${CMAKE_COMMAND} -E remove ${GITREV_TMP}
  WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
  VERBATIM
)
# Finally, the temporary file should be added as a dependency to the target

ADD_EXECUTABLE(test source.cpp ${GITREV_FILE})

(Edit: update GITREV_TMP to GITREV_FILE in ADD_EXECUTABLE)

Solution 4 - Git

I'd use something like this in my CMakeLists.txt:

execute_process(
    COMMAND git describe
    WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
    OUTPUT_VARIABLE VERSION )

string( REGEX MATCH "-g.*$" VERSION_SHA1 ${VERSION} )
string( REGEX REPLACE "[-g]" "" VERSION_SHA1 ${VERSION_SHA1} )

add_definitions( -DGIT_SHA1="${VERSION_SHA1}" )

Solution 5 - Git

The following solution is based on the observation that Git updates the HEAD log whenever you pull or commit something. Note that e.g. Drew's suggestion above will update the Git information only if you rebuild the CMake cache manually after every commit.

I use a CMake "custom command" that generates a one-line header file ${SRCDIR}/gitrevision.hh where ${SRCDIR} is the root of your source tree. It will be re-made only when a new commit is made. Here is the necessary CMake magic with some comments:

# Generate gitrevision.hh if Git is available
# and the .git directory is present
# this is the case when the software is checked out from a Git repo
find_program(GIT_SCM git DOC "Git version control")
mark_as_advanced(GIT_SCM)
find_file(GITDIR NAMES .git PATHS ${CMAKE_SOURCE_DIR} NO_DEFAULT_PATH)
if (GIT_SCM AND GITDIR)
    # Create gitrevision.hh
    # that depends on the Git HEAD log
    add_custom_command(OUTPUT ${SRCDIR}/gitrevision.hh
        COMMAND ${CMAKE_COMMAND} -E echo_append "#define GITREVISION " > ${SRCDIR}/gitrevision.hh
        COMMAND ${GIT_SCM} log -1 "--pretty=format:%h %ai" >> ${SRCDIR}/gitrevision.hh
        DEPENDS ${GITDIR}/logs/HEAD
        VERBATIM
    )
else()
    # No version control
    # e.g. when the software is built from a source tarball
    # and gitrevision.hh is packaged with it but no Git is available
    message(STATUS "Will not remake ${SRCDIR}/gitrevision.hh")
endif()

The contents of gitrevision.hh will look like this:

#define GITREVISION cb93d53 2014-03-13 11:08:15 +0100

If you want to change this then edit the --pretty=format: specification accordingly. E.g. using %H instead of %h will print the full SHA1 digest. See the Git manual for details.

Making gitrevision.hh a fully-fledged C++ header file with include guards etc. is left as an exercise to the reader :-)

Solution 6 - Git

##Solution## Simply adding some code to only 2 files: CMakeList.txt and main.cpp.

###1. CMakeList.txt###

# git commit hash macro
execute_process(
  COMMAND git log -1 --format=%h
  WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
  OUTPUT_VARIABLE GIT_COMMIT_HASH
  OUTPUT_STRIP_TRAILING_WHITESPACE
)
add_definitions("-DGIT_COMMIT_HASH=\"${GIT_COMMIT_HASH}\"")

###2. main.cpp###

inline void LogGitCommitHash() {
#ifndef GIT_COMMIT_HASH
#define GIT_COMMIT_HASH "0000000" // 0000000 means uninitialized
#endif
	std::cout << "GIT_COMMIT_HASH[" << GIT_COMMIT_HASH << "]"; // 4f34ee8
}

##Explanation## In CMakeList.txt, the CMake commandexecute_process() is used to call command git log -1 --format=%h that give you the short and unique abbreviation for your SHA-1 values in string like 4f34ee8. This string is assigned to CMake variable called GIT_COMMIT_HASH. The CMake command add_definitions() defines the macro GIT_COMMIT_HASH to the value of 4f34ee8 just before gcc compilation. The hash value is used to replace the macro in C++ code by preprocessor, and hence exists in the object file main.o and in the compiled binaries a.out.

##Side Note## Another way to achieve is to use CMake command called configure_file(), but I don't like to use it because the file does not exist before CMake is run.

Solution 7 - Git

Here's my solution, which I think is reasonably short yet effective ;-)

First, a file is needed in the source tree (I name it git-rev.h.in), it should looks something like this:

#define STR_EXPAND(x) #x
#define STR(x) STR_EXPAND(x)
#define GIT_REV STR(GIT_REV_)
#define GIT_REV_ \ 
 

(Please never mind those macros, that's a little bit insane trick to make a string out of a raw value.) It is essential that this file has exactly one empty newline at the end so that value can be appended.

And now this code goes in respective CMakeLists.txt file:

# --- Git revision ---
add_dependencies(your_awesome_target gitrev)      #put name of your target here
include_directories(${CMAKE_CURRENT_BINARY_DIR})  #so that the include file is found
set(gitrev_in git-rev.h.in)                       #just filenames, feel free to change them...
set(gitrev git-rev.h)
add_custom_target(gitrev
  ${CMAKE_COMMAND} -E remove -f ${CMAKE_CURRENT_BINARY_DIR}/${gitrev}
  COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/${gitrev_in} ${CMAKE_CURRENT_BINARY_DIR}/${gitrev}
  COMMAND git rev-parse HEAD >> ${CMAKE_CURRENT_BINARY_DIR}/${gitrev}
  WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}         #very important, otherwise git repo might not be found in shadow build
  VERBATIM                                              #portability wanted
)

This command ensuers that the git-rev.h.in is copied in the build tree as git-rev.h and git revision is appended at its end.

So all you need to do next is include git-rev.h in one of your files and do whatever you want with the GIT_REV macro, which yields current git revision hash as a string value.

The nice thing about this solution is that the git-rev.h is recreated each time you build the associated target, so you don't have to run cmake over and over again.

It also should be pretty portable - no non-portable external tools were used and even the bloody stupid windows cmd supports the > and >> operators ;-)

Solution 8 - Git

I can't help you with the CMake side, but with respect to Git side I would recommend taking a look how Linux kernel and Git project itself does it, via [GIT-VERSION-GEN][] script, or how tig does it in its [Makefile][tig-Makefile], by using git describe if there is git repository present, falling back to "version" / "VERSION" / "GIT-VERSION-FILE" generated and present in tarballs, finally falling back to default value hardcoded in script (or Makefile).

The first part (using git describe) requires that you tag releases using annotated (and possibly GPG signed) tags. Or use git describe --tags to use also lightweight tags.

[GIT-VERSION-GEN]: http://git.kernel.org/?p=git/git.git;a=blob;f=GIT-VERSION-GEN;hb=master "git.kernel.org - git/git.git/blob - GIT-VERSION-GEN (gitweb)" [tig-Makefile]: http://repo.or.cz/w/tig.git?a=blob;f=Makefile "Public Git Hosting - tig.git/blob - Makefile (gitweb)"

Solution 9 - Git

If CMake doesn't have a built-in capability to do this substitution, then you could write a wrapper shell script that reads a template file, substitutes the SHA1 hash as above in the correct location (using sed, for example), creates the real CMake build file, and then calls CMake to build your project.

A slightly different approach might be to make the SHA1 substitution optional. You would create the CMake file with a dummy hash value such as "NO_OFFICIAL_SHA1_HASH". When developers build their own builds from their working directories, the built code would not include a SHA1 hash value (only the dummy value) because the code from the working directory doesn't even have a corresponding SHA1 hash value yet.

On the other hand, when an official build is made by your build server, from sources pulled from a central repository, then you know the SHA1 hash value for the source code. At that point, you can substitute the hash value in the CMake file and then run CMake.

Solution 10 - Git

For a quick and dirty, possibly not portable way to get the git SHA-1 into a C or C++ project using CMake, I use this in CMakeLists.txt:

add_custom_target(git_revision.h
 git log -1 "--format=format:#define GIT_REVISION \"%H\"%n" HEAD > git_revision.h
 WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} VERBATIM)

It assumes that CMAKE_SOURCE_DIR is part of a git repository, and that git is available on the system, and that an output redirection will be properly parsed by the shell.

You can then make this target a dependency of any other target using

add_dependencies(your_program git_revision.h)

Each time your_program is built, the Makefile (or other build system, if this works on other build systems) will recreate git_revision.h in the source directory, with the contents

#define GIT_REVISION "<SHA-1 of the current git revision>"

So you can #include git_revision.h from some source code file and use it that way. Note that the header is created at literally every build, i.e. even if every other object file is up to date, it will still run this command to recreate git_revision.h. I figure that shouldn't be a huge problem because usually you don't rebuild the same git revision over and over again, but it's something to be aware of, and if it is a problem for you, then don't use this. (It's probably possible to hack up a workaround using add_custom_command but I haven't needed it so far.)

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionŁukasz LewView Question on Stackoverflow
Solution 1 - GitRyan PavlikView Answer on Stackoverflow
Solution 2 - GitDrew NoakesView Answer on Stackoverflow
Solution 3 - GitMark DewingView Answer on Stackoverflow
Solution 4 - Gitthe RitzView Answer on Stackoverflow
Solution 5 - GitLaryx DeciduaView Answer on Stackoverflow
Solution 6 - GitetorickyView Answer on Stackoverflow
Solution 7 - GitkralykView Answer on Stackoverflow
Solution 8 - GitJakub NarębskiView Answer on Stackoverflow
Solution 9 - GitGreg HewgillView Answer on Stackoverflow
Solution 10 - GitDavid ZView Answer on Stackoverflow