How-To: timestamp and git-hash version esp32 firmware builds


Problem statement

The ESP-IDF (SDK for ESP32 firmware development) allows project versioning using multiple approaches. One of the easiest is to use the contents of ${PROJECT_DIR}/version.txt as the app version1.

This short post shows you how to get the project (app) versioning automatically follow something like: 20250302075450-e9a0ca0-dirty, in other words: <YYYYmmddHHMMSS>-<git_short_hash>-<repo_dirty_flag>.

Journey

Getting the necessary info in CMake is actually super easy2:

string(TIMESTAMP BUILD_TIMESTAMP "%Y%m%d%H%M%S")
execute_process(
    COMMAND git show -s --format=%h
    OUTPUT_STRIP_TRAILING_WHITESPACE
    OUTPUT_VARIABLE GIT_SHORT_REV
)
execute_process(
    COMMAND bash -c "git diff --quiet --ignore-submodules || echo '-dirty'"
    OUTPUT_STRIP_TRAILING_WHITESPACE
    OUTPUT_VARIABLE GIT_DIRTY_SUFFIX
)
set(GIT_FULL_REV_ID "${GIT_SHORT_REV}${GIT_DIRTY_SUFFIX}")

A bit thornier issue is how to have it consistently regenerated.

I wonder if there’s a better solution, but for now I’ve opted for trashing the CMake cache:

cmake_minimum_required(VERSION 3.16)

add_custom_target(force_reconfig
    # I'm not proud of this, but it's needed to get a fresh version every time
    COMMAND touch ${CMAKE_BINARY_DIR}/CMakeCache.txt
)

# ...

include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(e32wamb)

# Force reconfig every time
add_dependencies(e32wamb.elf force_reconfig)

This means that CMake re-runs every single time. A bit wasteful, perhaps, but also couldn’t find a better solution3.

Solution

Wrap it all together, and you get version.cmake:

# This makes the following variables available:
# - BUILD_TIMESTAMP = "%Y%m%d%H%M%S" (UTC)
# - GIT_SHORT_REV = 7 char revision short
# - GIT_DIRTY_SUFFIX = "-dirty" if there are uncommitted changes
# - GIT_FULL_REV_ID = "${GIT_SHORT_REV}${GIT_DIRTY_SUFFIX}"
# which is useful for project versioning.
#
# If no git is available (or not within repo), GIT_DIRTY_SUFFIX
# and GIT_FULL_REV_ID are set to "nogit".
#
# Also, if this runs within docker (the project is under /project), then the
# /project dir and /opt/.../openthread are marked safe for git. Which avoids
# a verbose failure (to obtain the needed info).
#
# Also provides `force_reconfig` custom target that forces CMake reconfig every
# time a given dependency gets built. That's needed to keep the above variables
# fresh. But you need to plug it in as dependency of your main binary.
#
# ---
#
# Written in 2025 by Michal Jirků (wejn)
# License: CC0 or public domain (whatever's more pleasant for you)
#
# ---
#
# See
# https://wejn.org/2025/03/howto-timestamp-and-githash-esp32-firmware-builds/
# for background.

cmake_minimum_required(VERSION 3.16)

find_package(Git QUIET)

# Gather build timestamp + git revision
string(TIMESTAMP BUILD_TIMESTAMP "%Y%m%d%H%M%S")
set(GIT_DIRTY_SUFFIX "nogit")
set(GIT_SHORT_REV "")
set(GIT_FULL_REV_ID "nogit")

if(GIT_FOUND)
    # Mark the directories safe for git
    if(CMAKE_CURRENT_SOURCE_DIR STREQUAL "/project")
        execute_process(
            COMMAND ${GIT_EXECUTABLE} config --global --add safe.directory /project
            COMMAND ${GIT_EXECUTABLE} config --global --add safe.directory /opt/esp/idf/components/openthread/openthread
        )
    endif()

    execute_process(
        COMMAND ${GIT_EXECUTABLE} rev-parse --is-inside-work-tree
        RESULT_VARIABLE GIT_REPO_CHECK
        OUTPUT_STRIP_TRAILING_WHITESPACE
        OUTPUT_VARIABLE GIT_REPO_STATUS
    )

    if(GIT_REPO_CHECK EQUAL 0 AND GIT_REPO_STATUS STREQUAL "true")
        execute_process(
            COMMAND ${GIT_EXECUTABLE} show -s --format=%h
            OUTPUT_STRIP_TRAILING_WHITESPACE
            OUTPUT_VARIABLE GIT_SHORT_REV
        )
        execute_process(
            COMMAND bash -c "${GIT_EXECUTABLE} diff --quiet --ignore-submodules || echo '-dirty'"
            OUTPUT_STRIP_TRAILING_WHITESPACE
            OUTPUT_VARIABLE GIT_DIRTY_SUFFIX
        )

        set(GIT_FULL_REV_ID "${GIT_SHORT_REV}${GIT_DIRTY_SUFFIX}")
    endif()
endif()


add_custom_target(force_reconfig
    # I'm not proud of this, but it's needed to get a fresh version every time
    COMMAND touch ${CMAKE_BINARY_DIR}/CMakeCache.txt
)

Which you can use in your project thusly (CMakeLists.txt):

cmake_minimum_required(VERSION 3.16)

include(${CMAKE_CURRENT_SOURCE_DIR}/version.cmake)

set(PROJECT_VER "${BUILD_TIMESTAMP}-${GIT_FULL_REV_ID}")
message(STATUS "Project Version: ${PROJECT_VER}")
message(STATUS "Date code: ${BUILD_TIMESTAMP}")
message(STATUS "Git rev: ${GIT_FULL_REV_ID}")

set(EXTRA_COMPONENT_DIRS
    ${CMAKE_CURRENT_SOURCE_DIR}/light_driver
    )
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(e32wamb)

# Force reconfig every time
add_dependencies(e32wamb.elf force_reconfig)

To also use these things in your project you can do the following (in main/CMakeLists.txt):

#[...]

# Define `BUILD_FULL_REV` to be used in the source
add_definitions(-DBUILD_FULL_REV="${BUILD_TIMESTAMP}-${GIT_FULL_REV_ID}")

I’ve thrown it in a esp-app-better-versioning repo along with an example. Enjoy.

Closing words

I’m posting this small-ish thing in the hopes it’s useful to someone.

The custom Hue light driver is chugging along, even though it was derailed a bit by me being ill for a few weeks. Watch this space. ;)

  1. Which is amazing if you want to manually version, but less amazing when you can do without yet another thing to manually track.

  2. Barely an inconvenience; especially when you completely ignore any and all error checking.

  3. Happy to be on the receiving side of one, if you happen to be a CMake + ESP-IDF wizard.