How to build got revision into binary for version output?

Hi all,

first post, please be gentle…

When building, I need to include the git HEAD’s commit hash into a header, so I can have the program print out its version/commit (and whether the tree was dirty or not) on startup. For C++/CMAKE projects, I use GetGitRevisionDescription.cmake. Is there something like this in platformio?

I was thinking to use “extra_scripts = pre:custom.py” and have that append the output of “git rev-parse HEAD” to a file that already contains "#define GIT_SHA1 ". That would work, right?

And then I thought it’d be nice to put this magic into a library, so that I can just include e.g. the version.h from that library and re-use that in multiple projects.

And then it seems like this probably has been done before, or maybe even is a standard feature in platformio that haven’t found yet?

What’s the best way of doing this?

Thanks!

Depends… is being slapped with a wet fish considered gentle? :fishing_pole_and_fish: :laughing: Welcome to the forum! :wink:

@sblantipodi made this automation that you can plug in for a self-incrementing version number.

It sounds like that would be a good starting point to make something that grabs the commit/tag version/tree state, or even another feature that could be added.

Thanks pfeerick! Looks like @sblantipodi is doing this using the same extra_scripts hook, so I tried something along those lines. Created a “version” library/folder, contents are:

version.h

#pragma once
#include <string>
class Version {
  public:
    static std::string getGitCommitSha1();
    static std::string getBuildTimestamp();
};

custom.py

import datetime
import os
import subprocess

Import("env")

VERSION_FILE = 'version.cpp'

result = subprocess.run(['git', 'rev-parse', 'HEAD'], stdout=subprocess.PIPE).stdout.decode('utf-8')

VERSION_CONTENTS = """
#include "version.h"
std::string Version::getGitCommitSha1() {
 return "{}";
}
std::string Version::getBuildTimestamp() {
 return "{}";
}
""".format(result, datetime.datetime.now())

if os.environ.get('PLATFORMIO_INCLUDE_DIR') is not None:
    VERSION_FILE = os.environ.get('PLATFORMIO_INCLUDE_DIR') + os.sep + VERSION_FILE
elif os.path.exists("include"):
    VERSION_FILE = "include" + os.sep + VERSION_FILE
else:
    PROJECT_DIR = env.subst("$PROJECT_DIR")
    os.mkdir(PROJECT_DIR + os.sep + "include")
    VERSION_FILE = "include" + os.sep + VERSION_FILE

print("Updating {} with version/timestamp...".format(VERSION_FILE))
with open(VERSION_FILE, 'w+') as FILE:
        FILE.write(VERSION_CONTENTS)

platformio.ini

extra_scripts = pre:custom.py

The problem is when I include version.h from my project, it builds, but linking fails because undefined reference to Version::getGitCommitSha1[abi:cxx11](). And I cannot find a version.cpp, or the output of custom.py’s print(). Do I need to explicitly tell platformio to evaluate the platform.ini of the version-library?

If you’re using *nix (macOS should work too, but can’t check due to lack of hardware ;-)) you can use some shell “magic” and add something like that

build_flags = !echo "-DSOME_MACRO="$(git log |head -1 |cut -c8-)

to your platformio.ini. If you start your build SOME_MACRO will be set to commit hash and you can use it anywhere in your sourcecode without the need of an additional include file.

g++ -o .pio/build/native/src/json_consts.o -c -DPLATFORMIO=40304 -DSOME_MACRO=84c84ff34077768c89b5e2b82122db53fa3b8ff5 -Iinclude -Isrc -I.pio/libdeps/native/ArduinoJson/src -I.pio/libdeps/native/googletest -I.pio/libdeps/native/googletest/googlemock/include -I.pio/libdeps/native/googletest/googlemock -I.pio/libdeps/native/googletest/googletest/include -I.pio/libdeps/native/googletest/googletest -I.pio/libdeps/native/ArduinoFake_ID1689/src src/json_consts.cpp

3 Likes

I solved this problem with git describe and the advanced scripting.

First off, I use git tags for versioning. I dont bother with manually writing it anywhere, it should all be based on the git tags. This makes CI and such really easy, Since I never forget to update the version in some file somewhere.

Here is an example:

platformio.ini

[env:my_env]
platform = teensy
board = teensy40
framework = arduino
extra_scripts = 
    pre:auto_firmware_version.py

auto_firmware_version.py

import subprocess

Import("env")

def get_firmware_specifier_build_flag():
    ret = subprocess.run(["git", "describe"], stdout=subprocess.PIPE, text=True) #Uses only annotated tags
    #ret = subprocess.run(["git", "describe", "--tags"], stdout=subprocess.PIPE, text=True) #Uses any tags
    build_version = ret.stdout.strip()
    build_flag = "-D AUTO_VERSION=\\\"" + build_version + "\\\""
    print ("Firmware Revision: " + build_version)
    return (build_flag)

env.Append(
    BUILD_FLAGS=[get_firmware_specifier_build_flag()]
)

main.cpp

#include <Arduino.h>

void setup(){
    serial.begin(115200);
    serial.print("Firmware Version: ");
    serial.println(AUTO_VERSION);   // Use the preprocessor directive

   // OR //

   char firmware_char_array[] = AUTO_VERSION;
   serial.println(firmware_char_array, sizeof(firmware_char_array));
}

void loop(){
    // Loop
}

With this configuration, you get the firmware version as a string literal. You can use it however you want since it is dealt with in the preprocessor and not compiler.


This, for example, will print the tag that the commit is aligned with:

v1.2.3

or, if there isnt a tag at the commit, the relation to the latest tag:

v1.2.3-13-gabc1234
└────┤ └┤  └─────┴─ Short commit Hash (not the g)
     │  └─ Distance from tag
     └─ Latest Tag in Git

You can customize this string however you like in the python script, For example:

build_version = "My_Project_Firmware-" + ret.stdout.strip() + "-" + env['PIOENV'].upper()

would produce:

My_Project_Firmware-v1.2.3-13-gabc1234-MY_ENV

I use the env['PIOENV'] to distinguish between different build environments, Useful if you have regular builds and debug builds.

3 Likes

I’ve been using the git describe command in my scripts, but now I want my client to build the code on their own and it will not work if they are running VS Code + PlatformIO in an ordinary Windows computer.

After some research, I’ve found the dulwich python package. It’s a simplified git client in python.
I adapted @awbmilne code to use the dulwich package and the final result is:

platformio.ini

[env:my_env]
platform = teensy
board = teensy40
framework = arduino
extra_scripts = 
    pre:auto_firmware_version.py

auto_firmware_version.py

import pkg_resources

Import("env")

required_pkgs = {'dulwich'}
installed_pkgs = {pkg.key for pkg in pkg_resources.working_set}
missing_pkgs = required_pkgs - installed_pkgs

if missing_pkgs:
    env.Execute('$PYTHONEXE -m pip install dulwich --global-option="--pure"')

from dulwich import porcelain

def get_firmware_specifier_build_flag():
    build_version = porcelain.describe('.')  # '.' refers to the repository root dir
    build_flag = "-D AUTO_VERSION=\\\"" + build_version + "\\\""
    print ("Firmware Revision: " + build_version)
    return (build_flag)

env.Append(
    BUILD_FLAGS=[get_firmware_specifier_build_flag()]
)

main.cpp

#include <Arduino.h>

void setup(){
    serial.begin(115200);
    serial.print("Firmware Version: ");
    serial.println(AUTO_VERSION);   // Use the preprocessor directive

   // OR //

   char firmware_char_array[] = AUTO_VERSION;
   serial.println(firmware_char_array, sizeof(firmware_char_array));
}

void loop(){
    // Loop
}

The limitation is that this git describe implementation is a little limited, for instance it doesn’t show if the repository is “dirty”. But anyway, it runs in Linux and Windows.

2 Likes

Good tip. Thanks.

It was what I was looking for.