Skip to content

Instantly share code, notes, and snippets.

@Len42
Last active January 28, 2023 21:31
Show Gist options
  • Save Len42/0d53fee0e04787451187b5ed5fe0a4a5 to your computer and use it in GitHub Desktop.
Save Len42/0d53fee0e04787451187b5ed5fe0a4a5 to your computer and use it in GitHub Desktop.
MakeVersionInfo - Update a project's version based on the latest git tag (CMake & Python)

MakeVersionInfo

Python / Cmake utility to automatically update a project's version number based on the latest git tag

How To

  • Put make-version-file.py and update-version-info.py in your project directory.
  • Insert CMakeLists.snippet into your CMakeLists.txt file. Modify it as necessary.
  • Create a template file named <outputfile>.template to define the format of your version info file. See version.h.template for example.
  • Define the version number of your project by making a git tag with either git tag or a GitHub release. The tag must be named something like "1.0" or "1.2.3" or "v1.2.3".
  • Build your project. The output file (e.g. version.h) will be created with definitions for the latest version info, which you can use in your code.

Notes

The output file (version.h or whatever) will only be updated when necessary, i.e. when the git repository is modified. This ensures that your project files which use version.h are only re-compiled when the version info changes, not every time you do a build.

See the example version.h.template file for a list of the available version info parameters. Since this uses Python's formatted string syntax, curly braces "{}" are special and if you want a curly brace in your output file, it must be doubled in the template file "{{ }}".

If the latest commit to the git repo isn't explicitly tagged, a build number and commit hash will be generated from git describe to precisely identify the source code version. If there are un-committed changes in the repo at build time, verIsDevBuild is set to 1 and "-dev" is appended to verString.

If you end up with the version number 0.0.0.0, it's probably because the most recent git tag doesn't look like a version number.

# Insert this in your CMakeLists.txt file, after add_executable():
# Update version info file from the latest git tag
set(VERSION_FILE "version.h") # CHANGE to whatever you want
target_sources(MyExecutableName PRIVATE ${VERSION_FILE}) # CHANGE MyExecutableName
set(CMD_PYTHON "py") # for Windows - CHANGE if necessary
#set(CMD_PYTHON "/usr/local/bin/python3.11") # for macOS, Linux
set(VERSION_TEMP_FILE "${PROJECT_BINARY_DIR}/version-temp")
set(VERSION_INFO_FILE "${PROJECT_BINARY_DIR}/version-info")
add_custom_target(MakeVersionFile
COMMAND git describe --tags --always --dirty >${VERSION_TEMP_FILE}
COMMAND ${CMD_PYTHON} ${PROJECT_SOURCE_DIR}/update-version-info.py
${VERSION_INFO_FILE}
${VERSION_TEMP_FILE}
)
add_dependencies(MyExecutableName MakeVersionFile) # CHANGE MyExecutableName
add_custom_command(
OUTPUT ${PROJECT_SOURCE_DIR}/${VERSION_FILE}
DEPENDS ${VERSION_INFO_FILE} ${PROJECT_SOURCE_DIR}/${VERSION_FILE}.template
COMMAND ${CMD_PYTHON} ${PROJECT_SOURCE_DIR}/make-version-file.py
${VERSION_INFO_FILE}
${PROJECT_SOURCE_DIR}/${VERSION_FILE}
)
# make-version-file
# Writes a file with inserted version information.
# Usage: make-version-file.py <version-info-filename> <output-filename>
# <version-info-filename> is a version info file written by update-version-info.py
# <output-filename> will be written with the latest version info
# A template file named "<output-filename>.template" is expected, containing
# text with placeholders for the version info. The supported placeholders are:
# {verMajor}
# {verMinor}
# {verRevision}
# {verBuild}
# {verCommit}
# {verIsDevBuild}
# {verString}
# {verTimestamp}
# {verDatestamp}
import sys
import os
import datetime
cmdName, verInfoFile, *outputFiles = sys.argv
cmdName = os.path.basename(cmdName)
try:
with open(verInfoFile, 'r') as file:
versionGit = file.readline().strip()
except:
versionGit = '0.0.0-0-unknown-dirty'
# Parse the git version info.
verMajor = 0
verMinor = 0
verRevision = 0
verBuild = 0
verCommit = ''
verIsDevBuild = False
# versionGit comes from 'git describe --tags'.
# (Tags in the repo are expected to be version numbers.)
# We interpret it like this: <major>.<minor>.<rev>-<build>-<commit>-<dirty>
# Some of those items may be missing.
# Examples of what it may look like:
# g12345
# 0
# 0.0
# 0.0.0
# 0.0-2-g12345
# 0.0-2-g12345-dirty ('-dirty' indicates that there are un-committed changes)
# Version tag may start with a 'v', e.g. 'v1.2'. Remove it.
if versionGit.startswith('v'):
versionGit = versionGit[1:]
parts = versionGit.split('-')
if parts[-1] == 'dirty':
verIsDevBuild = True
del parts[-1]
# Parse the tag into numbers
nums = parts[0].split('.')
verMajor = int(nums[0]) if nums[0].isdigit() else 0
if len(nums) > 1:
verMinor = int(nums[1]) if nums[1].isdigit() else 0
if len(nums) > 2:
verRevision = int(nums[2]) if nums[2].isdigit() else 0
# Parse the other parts (there should be zero or two parts)
if len(parts) > 2:
# Expect a number of commits (which we use as a build number)
# and a commit hash
verBuild = int(parts[1]) if parts[1].isdigit() else 0
verCommit = parts[2]
verString = '{verMajor}.{verMinor}.{verRevision}.{verBuild}{commit}{isDevBuild}'\
.format(\
verMajor = verMajor,\
verMinor = verMinor,\
verRevision = verRevision,\
verBuild = verBuild,\
commit = ('-' + verCommit) if verCommit else '',\
isDevBuild = '-dev' if verIsDevBuild else '')
tNow = datetime.datetime.now(datetime.timezone.utc)
verTimestamp = tNow.isoformat()
verDatestamp = tNow.date().isoformat()
# Write the output file, based on a template file.
# str.format() will replace the given placeholders with the corresponding
# version info items.
for outputFile in outputFiles:
templateFile = outputFile + '.template'
try:
with open(templateFile, 'r') as file:
input = file.read()
except:
# If there's no template file, output some generic text.
print(f'{cmdName}: No template file was found for {outputFile}')
input = 'Version {verString} updated {verDatestamp}\n'
output = input.format(\
verMajor = verMajor,\
verMinor = verMinor,\
verRevision = verRevision,\
verBuild = verBuild,\
verCommit = verCommit,\
verIsDevBuild = 1 if verIsDevBuild else 0,\
verString = verString,\
verTimestamp = verTimestamp,\
verDatestamp = verDatestamp)
with open(outputFile, 'w') as file:
file.write(output)
# update-version-info
# Updates a file with version information derived from a git release tag.
# Usage: make-version-file.py <version-info-filename> <git-describe-filename>
# <version-info-filename> is a file used to save version info
# <git-describe-filename> is a file containing the output from 'git describe --tags --always --dirty'
# <version-info-filename> will be created or updated if necessary with the latest
# version information.
# The output file will only be written if necessary, i.e. if it doesn't exist or
# if the version info needs to be updated. This will prevent code from being
# re-compiled unnecessarily due to the version.h file being written every time.
# This utility gets the version info from the most recent git tag which should
# be a version number like '1.2.3'.
import sys
import os
cmdName, fnameSavedVer, fnameGitVer = sys.argv
cmdName = os.path.basename(cmdName)
try:
with open(fnameGitVer, 'r') as file:
versionGit = file.readline().strip()
except:
versionGit = '0.0.0-0-unknown-dirty'
try:
with open(fnameSavedVer, 'r') as file:
versionSaved = file.readline().strip()
except:
versionSaved = ''
# Only update the output file if necessary, to avoid unnecessary recompilations.
if versionGit == versionSaved:
pass # no need to update the version info
else:
print(f'{cmdName}: Updating version info')
with open(fnameSavedVer, 'w') as file:
file.write(versionGit + '\n')
#define VER_MAJOR {verMajor}
#define VER_MINOR {verMinor}
#define VER_REVISION {verRevision}
#define VER_BUILD {verBuild}
#define VER_COMMIT "{verCommit}"
#define VER_DEV_BUILD {verIsDevBuild}
#define VER_STRING "{verString}"
#define VER_DATESTAMP "{verDatestamp}"
#define VER_TIMESTAMP "{verTimestamp}"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment