Every software product needs a version number. Once bug reports start rolling in, a version number is an absolute requirement for debugging and addressing issues
In this guide, I describe the details I like to store in each build, how I capture these details, and how I make them available to the firmware.
Table of Contents:
- What to Capture
- Getting Version into Source
- How do I generate my version automatically?
- Is there a simpler approach without git tags?
- Triggering Version Updates Every Time We Run Make
- On Deterministic Builds
- CMake Approach
- Further Reading
What to Capture
Here are the details I like to log for each build:
- Time/date of build
- Machine that built it
- Version (git tag number)
- Number of commits away from the last tag
- Commit hash
- Dirty or clean build (dirty = local changes that are not committed)
With this set of information, I get a full picture of the build’s history and can answer questions such as:
- Was the build created by the server, or locally by Joe Smith?
- Is it a clean build, or are there changes that weren’t committed?
- Is it a tagged build, or are there extra commits since the last build?
We leave “Time/date of build” for historical purposes, but we do not recommend embedding the date into your binary because it prevents you from implementing Reproducible Builds.
Timestamp
The easiest way to get the timestamp is to run the date
shell command:
date
Thu Oct 27 10:27:40 PDT 2016
To include the string in a Makefile:
-DVERSION_BUILD_DATE=\""$(shell date)"\"
We leave this for historical purposes, but we do not recommend embedding the date into your binary because it prevents you from implementing Reproducible Builds.
Build Machine
For build machine details, I use the whoami
and hostname
commands:
hostname
commands:
whoami
jenkins
hostname
buildbot
I take these two values and combine them into a single string:
jenkins@buildbot
To include the string in a Makefile:
-DVERSION_BUILD_MACHINE=\""$(shell echo `whoami`@`hostname`)"\"
Version
Conveniently, you can get the version information directly from git:
git describe --long --dirty --tags
0.1.64-2-gb27efef
Above, 0.1.64 is the latest version tag, 2 indicates that I am two commits ahead of the last versioned build, and the commit hash is at the end.
The simplest approach is to pass the entire git describe
output into your version string, but I think it makes your version more confusing to customers, vendors, and CMs. The git describe
output is also too verbose if you all you want is to display a marketing version, such as “1.2.64”.
I store the following values separately:
BUILD_TAG
(0.1.64)COMMIT
(gb27efef
)COMMITS_PAST
(2)DIRTY_FLAG
(+ if dirty, empty if clean)
Here’s some example Makefile code to make these values available:
version := $(subst -, ,$(shell git describe --long --dirty --tags))
COMMIT := $(strip $(word 3, $(version)))
COMMITS_PAST := $(strip $(word 2, $(version)))
DIRTY := $(strip $(word 4, $(version)))
ifneq ($(COMMITS_PAST), 0)
BUILD_INFO_COMMITS := "."$(COMMITS_PAST)
endif
ifneq ($(DIRTY),)
BUILD_INFO_DIRTY :="+"
endif
export BUILD_TAG :=$(strip $(word 1, $(version)))
export BUILD_INFO := $(COMMIT)$(BUILD_INFO_COMMITS)$(BUILD_INFO_DIRTY)
This will result in:
Build version: 0.1.64
Build info: gb27efef.2
If the build was dirty, you’d see this instead:
Build info: gb27efef.2+
Modifications to Support Windows
If you’re using Windows, you will need to use different commands to get the BUILD_DATE
and BUILD_MACHINE
variables.
BUILD_DATE := $(shell python -c"from datetime import datetime; print(datetime.utcnow().strftime('%d/%m/%Y, %H:%M'))")
BUILD_MACHINE := $(shell echo %username%)@$(shell hostname)
You can support both Windows and OSX/Linux using shell detection:
ifeq ($(SHELL), cmd.exe)
BUILD_DATE := $(shell python -c "from datetime import datetime; print(datetime.utcnow().strftime('%d/%m/%Y, %H:%M'))"
BUILD_MACHINE := $(shell echo %username%)@$(shell hostname)
else
BUILD_MACHINE := $(shell whoami)@$(shell hostname)
BUILD_DATE := $(shell date -u +"%d/%m/%Y, %H:%M")
endif
Getting Version into Source
Now that we have all the information we want to include for each build available in MAKE variables, we can pass this information to the compiler using compiler definitions.
In your makefile, create a set of definitions for your version library and add them to your CFLAGS:
CFLAGS += -DVERSION_BUILD_DATE=\""$(shell date)"\" \
-DVERSION_BUILD_MACHINE=\""$(shell echo `whoami`@`hostname`)"\" \
-DVERSION_TAG=\"$(BUILD_TAG\" \
-DVERSION_BUILD=\"$(BUILD_INFO)\"
In your source code, refer to these definitions:
void version_print()
{
printf("%s %s %s (%s)
", VERSION_BUILD_DATE, VERSION_BUILD_MACHINE, VERSION_TAG, VERSION BUILD);
}
And voilà! you have a version available.
Again, we no longer recommend embedding the date into your binary because it prevents you from implementing Reproducible Builds.
How do I generate my version automatically?
If you’re at the stage where you need version builds, you should also be using a CI server such as Jenkins to build your software. These automated build tools will give each build a unique version. You can use the version in your build process.
I create two parametric values for my builds:
MAJOR_VERSION
MINOR_VERSION
When combined with the Jenkins BUILD_NUMBER
variable, you can form your typical version triple by combing them:
${MAJOR_VERSION}.${MINOR_VERSION}.${BUILD_NUMBER}
Resulting in a string like:
1.2.48
The versioning strategy outlined in the article relies on git tags. We’ll use temporary tags during the build process to create a clean version number.
Before running the build, I make a temporary local tag:
git tag -a -f -m"Candidate build: ${MAJOR_VERSION}.${MINOR_VERSION}.${BUILD_NUMBER}" ${MAJOR_VERSION}.${MINOR_VERSION}.${BUILD_NUMBER}
make rtos
git tag -d ${MAJOR_VERSION}.${MINOR_VERSION}.${BUILD_NUMBER}
By creating an annotated tag before the build, git describe
will work as expected and return a clean “0.1.64” instead of “0.1.63–8”. After the build succeeds, push the tag to the host and so it will be available for the next build:
git tag -a -f -m"Successful nightly build ${MAJOR_VERSION}.${MINOR_VERSION}.${BUILD_NUMBER}" ${MAJOR_VERSION}.${MINOR_VERSION}.${BUILD_NUMBER}
git push origin ${MAJOR_VERSION}.${MINOR_VERSION}.${BUILD_NUMBER}
Is there a simpler approach without git tags?
You can pass the ${MAJOR_VERSION}.${MINOR_VERSION}.${BUILD_NUMBER}
variables into your make
command and utilize those definitions directly:
make rtos BUILD_TAG=${MAJOR_VERSION}.${MINOR_VERSION}.${BUILD_NUMBER}
And:
CFLAGS += -DVERSION_TAG=\"$(BUILD_TAG)\"
However, I always recommend pushing a tag for successful builds. Having each build tagged allows for easier debugging and investigation.
Triggering Version Updates Every Time We Run Make
This versioning scheme works well for build servers, since they are often configured to create versioned builds from a clean slate. However, if you are generating builds from the command line in an incremental manner, the version will not be updated each time you compile.
To trigger a recompilation at every step, we must get our build system to think the file is out of date. Modern build systems, such as Meson, provide options such as build_always_stale
which can be used to create a target that is run each build, whether the file is changed or not.
With make
, we must perform some trickery to get this to work.
One approach is to touch our version.c
file each time we run make
:
$(shell touch path/to/version.c)
Another approach is to set version.o
to use a PHONY
prerequisite which is not connected to a file:
version.o: FORCE
.PHONY: FORCE
FORCE:
On Deterministic Builds
For purposes of determinism, you will want to eliminate the timestamp information from the build. The timestamp will prevent a deterministic build output, since the binary will change on every build invocation.
CMake Approach
The Benchmark Space Systems team wrote in with improvements upon the approach outlined above. They migrated from using Make to CMake, and adjusted the version process accordingly. The attached snippet does everything described in the article above except for including the username in the BUILD_MACHINE
data.
execute_process(
COMMAND git describe --long --dirty --always
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
OUTPUT_VARIABLE RAW_GIT_VERSION
OUTPUT_STRIP_TRAILING_WHITESPACE
)
string(REPLACE "-dirty" "+" BUILD_INFO ${RAW_GIT_VERSION})
string(TIMESTAMP BUILD_DATE "%d-%m-%Y, %H:%M" UTC )
cmake_host_system_information(RESULT BUILD_MACHINE QUERY HOSTNAME)
string(COMPARE EQUAL ${BUILD_MACHINE} jenkins-server _cmp)
if (NOT _cmp)
string(PREPEND BUILD_INFO dev-)
endif()
string(REPLACE "-" ";" GIT_INFO_LIST ${RAW_GIT_VERSION})
list(GET GIT_INFO_LIST 0 VERSION_NUMBER)
message("VERSION NUMBER: ${VERSION_NUMBER}")
add_compile_definitions(VERSION_INFO="${VERSION_NUMBER}")
message("BUILD DATE: ${BUILD_DATE}")
add_compile_definitions(VERSION_BUILD_DATE="${BUILD_DATE}")
message("BUILD MACHINE: ${BUILD_MACHINE}")
add_compile_definitions(VERSION_BUILD_MACHINE="${BUILD_MACHINE}")
message("BUILD_INFO: ${BUILD_INFO}")
add_compile_definitions(VERSION_BUILD_INFO="${BUILD_INFO}")
Further Reading
- Paul Shepherd describes using this approach with Eclipse auto-generated Makefiles in Using Custom Build Steps with Eclipse Auto-generated Makefiles
- Wolfram Roesler suggests an alternative approach for setting a version at link time
- How to force Make to always rebuild a target
- Versioning Software
- Debugging Strategy: Finding Version from a Memory Dump – how to find a version number using your debugger, in the event that you cannot access a shell
- A Strategy for Reporting Version Information from Bootloaders
Introduction to Build Systems Using Make
Ready to step beyond the IDE? Our hands-on course will teach you the basics of the software construction process using compilers and linkers, the Make language, and how build systems help us to automate the process while avoiding common build errors.
Learn More on the Course Page
How do you read the version back from the compiled binary?
Well, that’s up to you! There are many ways I’ve seen it done: reading it directly from the binary using
strings
, printing the version to the terminal/log on boot, creating a shell command to display the version, having an endpoint to query the version.