Giving Your Firmware Build a Version

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:

  1. What to Capture
    1. Timestamp
    2. Build Machine
    3. Version
    4. Modifications to Support Windows
  2. Getting Version into Source
  3. How do I generate my version automatically?
  4. Is there a simpler approach without git tags?
  5. Triggering Version Updates Every Time We Run Make
  6. On Deterministic Builds
  7. CMake Approach
  8. 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?
Warning

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)"\"
Warning

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 describeoutput 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.

Warning

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 makecommand 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

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

2 Replies to “Giving Your Firmware Build a Version”

  1. 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.

Share Your Thoughts

This site uses Akismet to reduce spam. Learn how your comment data is processed.