Versioning is a helpful guide to where, what and when a particular build came from. Knowing a particular build version can be especially useful when trying to replicate a particular users issue.

The first thing I’m going to show you, is how to pull the information from git so you can add it to a config file through cmake.

git.cmake:

# Get the repo url
execute_process(
  COMMAND git config --get remote.origin.url
  WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
  OUTPUT_VARIABLE GIT_REPO
  OUTPUT_STRIP_TRAILING_WHITESPACE
)

# Get the current working branch
execute_process(
  COMMAND git rev-parse --abbrev-ref HEAD
  WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
  OUTPUT_VARIABLE GIT_BRANCH
  OUTPUT_STRIP_TRAILING_WHITESPACE
)

# Get the latest abbreviated commit hash of the working branch
execute_process(
  COMMAND git log -1 --format=%h
  WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
  OUTPUT_VARIABLE GIT_COMMIT_HASH
  OUTPUT_STRIP_TRAILING_WHITESPACE
)

# Get the repo's Revision count
execute_process(
  COMMAND git rev-list HEAD --count
  WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
  OUTPUT_VARIABLE GIT_REV_COUNT
  OUTPUT_STRIP_TRAILING_WHITESPACE
)

You can also view the snippet here.

The execute_process is run when cmake generates the project or makefile. You can use cmake’s configure_file to transfer the variables above to a config header file.

Now with that in place we can work on the version numbers. We’ll be using a simple python script I wrote for the job:

versioner.py:

#!/bin/python3

import re
import io
import sys
import os

class Versioner:
    def __init__(self,root):
        self.version_major = 0
        self.version_minor = 0
        self.version_build = 0
        self.root = root        

    def readVersionFile(self):
        fo = open(self.root + "/VERSION", "r")
        line = fo.readline()
        x = re.findall("([0-9]+)\\.([0-9]+)\\.([0-9]+)", line)
        t = x[0]
        self.version_major = t[0]
        self.version_minor = t[1]
        self.version_build = t[2]
        
        print("Major: " + self.version_major)
        print("Minor: " + self.version_minor)
        print("Build: " + self.version_build)

        fo.close()

    def updateVersion(self):
        fo = open(self.root + "/VERSION", "w")
        build = int(self.version_build)
        build = build + 1
        self.version_build = str(build)

        vs = self.version_major + "." + self.version_minor + "." + self.version_build

        fo.writelines(vs)
        
        fo.close()        

        fo = open(self.root + "/cmake/versioner.cmake", "w")
        maj = "SET ( VERSION_MAJOR " + self.version_major + ")\r\n"
        min = "SET ( VERSION_MINOR " + self.version_minor + ")\r\n"
        build = "SET ( VERSION_BUILD " + self.version_build + ")\r\n"
        verstr = "SET ( VERSION \"" + vs +"\")\r\n"

        vstr = maj + min + build + verstr

        fo.writelines(vstr)
        fo.close()

    def createHeader(self, headerpath, header, project):
        if not os.path.exists(headerpath):
            os.makedirs(headerpath)
        filestr = headerpath + "/" + header
        uproject = project.upper()
        print (self.root + "/VERSION")
        
        fo = open(self.root + "/VERSION", "w")
        build = int(self.version_build)
        build = build + 1
        self.version_build = str(build)

        vs = self.version_major + "." + self.version_minor + "." + self.version_build

        fo.writelines(vs)

        fo.close()
        print ("header: " + header)
        fo = open(filestr, "w+")
        
        h = "#ifndef " + uproject + "_VERSION_H\r\n"
        h = h + "#define " + uproject + "_VERSION_H\r\n\r\n"

        maj = "#define " + uproject + "_VERSION_MAJOR " + self.version_major + ")\r\n"
        min = "#define " + uproject + "_VERSION_MINOR " + self.version_minor + ")\r\n"
        build = "#define " + uproject + "_VERSION_BUILD " + self.version_build + ")\r\n"
        verstr = "#define " + uproject + "_VERSION_STRING \"" + vs +"\")\r\n\r\n"

        f = "#endif //" + uproject + "_VERSION_H\r\n"

        vstr = h + maj + min + build + verstr + f

        fo.writelines(vstr)
        fo.close()
        

c = len(sys.argv)

if c == 2:
    r = sys.argv[1]
    v = Versioner(r)
    v.readVersionFile()
    v.updateVersion()
elif c == 5:
    root = sys.argv[1]    
    pp = sys.argv[2]
    f = sys.argv[3]
    project = sys.argv[4]
    v = Versioner(root)
    v.readVersionFile()
    v.createHeader(pp, f, project)
else:
    print("Incorrect number of variables\nargv.count: " + str(c))

You can also view this snippet here.

The purpose of this python script is to increment a build number from a file “VERSION”. This file should be of the format “MAJOR.MINOR.BUILD”. The major and minor is set by hand but the build we can either updated from the command line or let cmake do it.

$> cmake/versioner.py $PWD

The option passed to the script is just the root where the CMakeLists.txt is located.

We will however let cmake do it, and to tell it how, we do:

version.cmake:

if(NOT DEFINED VERSION_FILE)
        SET (VERSION_FILE "version.h")
endif(NOT DEFINED VERSION_FILE)

if(NOT DEFINED VERSION_FOLDER)
        SET (VERSION_FOLDER ${PROJECT_SOURCE_DIR}/include/${PROJECT_NAME})
endif(NOT DEFINED VERSION_FOLDER)

include(FindPythonInterp)
set (PyVersioner "${PROJECT_SOURCE_DIR}/cmake/version.py")

add_custom_target(
        pyversion ALL ${PYTHON_EXECUTABLE} ${PyVersioner} ${PROJECT_SOURCE_DIR} ${VERSION_FOLDER} ${VERSION_FILE} ${PROJECT_NAME}
        WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
)

You can also view this snippet here.

This creates a custom target, the key operator is the “ALL”, that will cause it to run on all build targets. The python script creates a header file directly at ${VERSION_FOLDER}/${VERSION_FILE}.

Combined, git version and build version numbering you’ll have all the information you need for a verbose versioning system.

The above code can be found: