From 97a6e27d56c6064a20e19a19efc5e55320021d4f Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Tue, 29 Sep 2026 15:43:08 -0500 Subject: [PATCH] build: generate a BuildInfo translation unit with version, commit, branch, dirty flag and build kind cmake/BuildInfo.cmake runs on every build and rewrites the generated BuildInfo.cpp only when something changed, so a new commit recompiles one file and relinks. The build kind comes from DLU_BUILD_KIND (release/ci/local) or CI=true, defaulting to local. Co-Authored-By: Claude Opus 5.5 --- cmake/BuildInfo.cmake | 71 ++++++++++++++++++++++++++++++++++++++++ dCommon/BuildInfo.cpp.in | 13 ++++++++ dCommon/BuildInfo.h | 51 +++++++++++++++++++++++++++++ dCommon/CMakeLists.txt | 22 ++++++++++++- 4 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 cmake/BuildInfo.cmake create mode 100644 dCommon/BuildInfo.cpp.in create mode 100644 dCommon/BuildInfo.h diff --git a/cmake/BuildInfo.cmake b/cmake/BuildInfo.cmake new file mode 100644 index 000000000..6a567e75b --- /dev/null +++ b/cmake/BuildInfo.cmake @@ -0,0 +1,71 @@ +# Writes BuildInfo.cpp (the data behind dCommon/BuildInfo.h) at build time. +# Run as a script: +# cmake -DSOURCE_DIR=... -DTEMPLATE=... -DOUTPUT=... -DVERSION_MAJOR=... -DVERSION_MINOR=... -DVERSION_PATCH=... -P BuildInfo.cmake +# The output is only rewritten when its contents change, so an unchanged tree recompiles nothing. +# +# Build kind comes from the environment of the build: DLU_BUILD_KIND=release|ci|local wins, otherwise CI=true +# (set by GitHub Actions and most CI systems) means ci, otherwise local. + +find_package(Git QUIET) + +set(BUILD_COMMIT "") +set(BUILD_BRANCH "") +set(BUILD_DIRTY "false") + +if(GIT_FOUND) + execute_process(COMMAND "${GIT_EXECUTABLE}" -C "${SOURCE_DIR}" rev-parse HEAD + OUTPUT_VARIABLE BUILD_COMMIT OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET RESULT_VARIABLE result) + if(NOT result EQUAL 0) + set(BUILD_COMMIT "") + endif() +endif() + +if(BUILD_COMMIT) + execute_process(COMMAND "${GIT_EXECUTABLE}" -C "${SOURCE_DIR}" rev-parse --abbrev-ref HEAD + OUTPUT_VARIABLE BUILD_BRANCH OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET) + if(BUILD_BRANCH STREQUAL "HEAD") + # Detached HEAD (CI checkouts): use the branch the CI names, if any. + if(NOT "$ENV{GITHUB_HEAD_REF}" STREQUAL "") + set(BUILD_BRANCH "$ENV{GITHUB_HEAD_REF}") + else() + set(BUILD_BRANCH "$ENV{GITHUB_REF_NAME}") + endif() + endif() + + execute_process(COMMAND "${GIT_EXECUTABLE}" -C "${SOURCE_DIR}" update-index -q --refresh OUTPUT_QUIET ERROR_QUIET) + execute_process(COMMAND "${GIT_EXECUTABLE}" -C "${SOURCE_DIR}" diff-index --quiet HEAD -- + RESULT_VARIABLE result OUTPUT_QUIET ERROR_QUIET) + if(NOT result EQUAL 0) + set(BUILD_DIRTY "true") + endif() +endif() + +string(TOLOWER "$ENV{DLU_BUILD_KIND}" kind) +string(TOLOWER "$ENV{CI}" ci) +if(kind STREQUAL "release") + set(BUILD_KIND "RELEASE") +elseif(kind STREQUAL "ci" OR (kind STREQUAL "" AND (ci STREQUAL "true" OR ci STREQUAL "1"))) + set(BUILD_KIND "CI") +else() + set(BUILD_KIND "LOCAL") +endif() + +# Readable identifier, e.g. 3.0.0-experimental+g1a2b3c4d-dirty. Release builds leave out the branch. +set(BUILD_STRING "${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}") +if(BUILD_BRANCH AND NOT BUILD_KIND STREQUAL "RELEASE") + string(REGEX REPLACE ".*/" "" branchName "${BUILD_BRANCH}") + string(REGEX REPLACE "[^0-9A-Za-z.-]" "-" branchName "${branchName}") + string(APPEND BUILD_STRING "-${branchName}") +endif() +if(BUILD_COMMIT) + string(SUBSTRING "${BUILD_COMMIT}" 0 8 shortCommit) + string(APPEND BUILD_STRING "+g${shortCommit}") + if(BUILD_DIRTY) + string(APPEND BUILD_STRING "-dirty") + endif() +endif() + +# Git allows " in branch names; keep the C++ string literal valid. +string(REPLACE "\"" "\\\"" BUILD_BRANCH "${BUILD_BRANCH}") + +configure_file("${TEMPLATE}" "${OUTPUT}" @ONLY) diff --git a/dCommon/BuildInfo.cpp.in b/dCommon/BuildInfo.cpp.in new file mode 100644 index 000000000..ce6c0154d --- /dev/null +++ b/dCommon/BuildInfo.cpp.in @@ -0,0 +1,13 @@ +// Generated by cmake/BuildInfo.cmake on every build. Do not edit. +#include "BuildInfo.h" + +namespace BuildInfo { + const uint8_t versionMajor = @VERSION_MAJOR@; + const uint8_t versionMinor = @VERSION_MINOR@; + const uint8_t versionPatch = @VERSION_PATCH@; + const eBuildKind buildKind = eBuildKind::@BUILD_KIND@; + const bool dirty = @BUILD_DIRTY@; + const std::string_view commit = "@BUILD_COMMIT@"; + const std::string_view branch = "@BUILD_BRANCH@"; + const std::string_view buildString = "@BUILD_STRING@"; +} diff --git a/dCommon/BuildInfo.h b/dCommon/BuildInfo.h new file mode 100644 index 000000000..99e5f9277 --- /dev/null +++ b/dCommon/BuildInfo.h @@ -0,0 +1,51 @@ +#ifndef BUILDINFO_H +#define BUILDINFO_H + +#include +#include + +// What this binary was built from. The values live in BuildInfo.cpp, which cmake/BuildInfo.cmake regenerates on +// every build (only rewriting it when something changed), so a new commit recompiles that one file and relinks. +namespace BuildInfo { + // Stored in 2 bits of Flags(). + enum class eBuildKind : uint8_t { + UNKNOWN = 0, + LOCAL = 1, + CI = 2, + RELEASE = 3, + }; + + constexpr uint8_t BUILD_KIND_MASK = 0b011; + constexpr uint8_t DIRTY_FLAG = 0b100; + + extern const uint8_t versionMajor; + extern const uint8_t versionMinor; + extern const uint8_t versionPatch; + extern const eBuildKind buildKind; + extern const bool dirty; // The build had uncommitted changes to tracked files + extern const std::string_view commit; // Full hex commit hash, empty without git + extern const std::string_view branch; // Empty when detached or without git + extern const std::string_view buildString; // e.g. 3.0.0-experimental+g1a2b3c4d-dirty + + // The build kind in bits 0-1 and the dirty flag in bit 2. + inline uint8_t Flags() { + return (static_cast(buildKind) & BUILD_KIND_MASK) | (dirty ? DIRTY_FLAG : 0); + } + + // The first 32 bits (8 hex digits) of the commit hash, 0 without git. + inline uint32_t CommitPrefix() { + uint32_t value = 0; + for (size_t i = 0; i < 8 && i < commit.size(); i++) { + const char c = commit[i]; + uint32_t digit = 0; + if (c >= '0' && c <= '9') digit = c - '0'; + else if (c >= 'a' && c <= 'f') digit = c - 'a' + 10; + else if (c >= 'A' && c <= 'F') digit = c - 'A' + 10; + else return 0; + value = (value << 4) | digit; + } + return commit.size() >= 8 ? value : 0; + } +} + +#endif // BUILDINFO_H diff --git a/dCommon/CMakeLists.txt b/dCommon/CMakeLists.txt index e106d1f76..6139d03f0 100644 --- a/dCommon/CMakeLists.txt +++ b/dCommon/CMakeLists.txt @@ -88,7 +88,27 @@ else () ) endif () +# Build identifier (BuildInfo.h). The generator runs on every build but only rewrites BuildInfo.cpp when the +# commit, branch, dirty state or build kind changed, so only that one file recompiles. +set(BUILDINFO_SOURCE "${CMAKE_CURRENT_BINARY_DIR}/generated/BuildInfo.cpp") +add_custom_target(BuildInfoGenerate + COMMAND "${CMAKE_COMMAND}" + "-DSOURCE_DIR=${PROJECT_SOURCE_DIR}" + "-DTEMPLATE=${CMAKE_CURRENT_SOURCE_DIR}/BuildInfo.cpp.in" + "-DOUTPUT=${BUILDINFO_SOURCE}" + "-DVERSION_MAJOR=${PROJECT_VERSION_MAJOR}" + "-DVERSION_MINOR=${PROJECT_VERSION_MINOR}" + "-DVERSION_PATCH=${PROJECT_VERSION_PATCH}" + -P "${PROJECT_SOURCE_DIR}/cmake/BuildInfo.cmake" + BYPRODUCTS "${BUILDINFO_SOURCE}" + COMMENT "Checking build identifier" + VERBATIM +) +add_library(dBuildInfo STATIC "${BUILDINFO_SOURCE}") +target_include_directories(dBuildInfo PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}") +add_dependencies(dBuildInfo BuildInfoGenerate) + target_link_libraries(dCommon - PUBLIC glm::glm + PUBLIC glm::glm dBuildInfo PRIVATE ZLIB::ZLIB bcrypt tinyxml2 INTERFACE dDatabase)