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. ;)
-
Which is amazing if you want to manually version, but less amazing when you can do without yet another thing to manually track. ↩
-
Barely an inconvenience; especially when you completely ignore any and all error checking. ↩
-
Happy to be on the receiving side of one, if you happen to be a CMake + ESP-IDF wizard. ↩