Giving Your Firmware Build a Version

Updated: 2019-03-22

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

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

Build Machine

For build machine details, I use the whoami and 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)\n", VERSION_BUILD_DATE, VERSION_BUILD_MACHINE, VERSION_TAG, VERSION BUILD);
}

And voilĂ ! you have a version available.

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:

Wolfram Roesler describes an alternative approach for setting the version when linking.

Further Reading

Change Log

  • 20190322:
    • Added Further Reading section
    • Added notes on out-of-date versions in CI builds.
    • Add links to other useful articles
  • 20190302:
    • Improved grammar
    • Corrected misspellings
    • Added table of contents
    • Added notes on supporting Windows

Related Articles