|
| 1 | +# ============================================================================ |
| 2 | +# UpdateProjectVersion.cmake |
| 3 | +# |
| 4 | +# Generic pre-build script that generates version information for any project. |
| 5 | +# Runs every build to capture: git commit counts, branch, hash, |
| 6 | +# dirty status and timestamp. |
| 7 | +# |
| 8 | +# Required input variables (set before including this script): |
| 9 | +# PROJECT_VERSION_PREFIX - Macro prefix, e.g. "Focal Engine" or "HabiCAT3D" |
| 10 | +# PROJECT_VERSION_MAJOR - Semantic version major |
| 11 | +# PROJECT_VERSION_MINOR - Semantic version minor |
| 12 | +# PROJECT_VERSION_PATCH - Semantic version patch |
| 13 | +# PROJECT_VERSION_DIR - Source directory (where the generated header will be placed) |
| 14 | +# |
| 15 | +# Optional: |
| 16 | +# PROJECT_VERSION_TEMPLATE_DIR - Directory containing ProjectVersion.h.in |
| 17 | +# (defaults to the directory of this script) |
| 18 | +# |
| 19 | +# Generates: |
| 20 | +# ${PROJECT_VERSION_DIR}/${PROJECT_VERSION_PREFIX}Version.h |
| 21 | +# |
| 22 | +# Produces version strings like: |
| 23 | +# On master, clean: "1.0.0 build 231 (ed4c7ce master)" |
| 24 | +# On master, dirty: "1.0.0 build 231 (ed4c7ce master, dirty)" |
| 25 | +# On branch, with offset: "1.0.0 build 231 (ed4c7ce dev +52 from master, dirty)" |
| 26 | +# ============================================================================ |
| 27 | + |
| 28 | +# --- Validate required inputs --- |
| 29 | +foreach(_var PROJECT_VERSION_PREFIX PROJECT_VERSION_MAJOR PROJECT_VERSION_MINOR PROJECT_VERSION_PATCH PROJECT_VERSION_DIR) |
| 30 | + if(NOT DEFINED ${_var}) |
| 31 | + message(FATAL_ERROR "UpdateProjectVersion.cmake: ${_var} must be set before including this script.") |
| 32 | + endif() |
| 33 | +endforeach() |
| 34 | + |
| 35 | +find_package(Git QUIET) |
| 36 | +if(GIT_FOUND) |
| 37 | + # --- Detect default branch name (master or main) --- |
| 38 | + # Try origin/master first, fall back to origin/main. |
| 39 | + execute_process( |
| 40 | + COMMAND ${GIT_EXECUTABLE} rev-parse --verify origin/master |
| 41 | + WORKING_DIRECTORY ${PROJECT_VERSION_DIR} |
| 42 | + RESULT_VARIABLE _MASTER_CHECK_RC |
| 43 | + OUTPUT_QUIET |
| 44 | + ERROR_QUIET |
| 45 | + ) |
| 46 | + if(_MASTER_CHECK_RC EQUAL 0) |
| 47 | + set(_DEFAULT_BRANCH "origin/master") |
| 48 | + else() |
| 49 | + execute_process( |
| 50 | + COMMAND ${GIT_EXECUTABLE} rev-parse --verify origin/main |
| 51 | + WORKING_DIRECTORY ${PROJECT_VERSION_DIR} |
| 52 | + RESULT_VARIABLE _MAIN_CHECK_RC |
| 53 | + OUTPUT_QUIET |
| 54 | + ERROR_QUIET |
| 55 | + ) |
| 56 | + if(_MAIN_CHECK_RC EQUAL 0) |
| 57 | + set(_DEFAULT_BRANCH "origin/main") |
| 58 | + else() |
| 59 | + set(_DEFAULT_BRANCH "") |
| 60 | + endif() |
| 61 | + endif() |
| 62 | + |
| 63 | + # --- Commit counts --- |
| 64 | + # Count total commits on the default branch - this is the stable build number. |
| 65 | + if(NOT "${_DEFAULT_BRANCH}" STREQUAL "") |
| 66 | + execute_process( |
| 67 | + COMMAND ${GIT_EXECUTABLE} rev-list --count ${_DEFAULT_BRANCH} |
| 68 | + WORKING_DIRECTORY ${PROJECT_VERSION_DIR} |
| 69 | + OUTPUT_VARIABLE _MASTER_COMMIT_COUNT |
| 70 | + OUTPUT_STRIP_TRAILING_WHITESPACE |
| 71 | + ERROR_QUIET |
| 72 | + ) |
| 73 | + endif() |
| 74 | + |
| 75 | + # Fallback: count all commits on current branch. |
| 76 | + if("${_MASTER_COMMIT_COUNT}" STREQUAL "") |
| 77 | + execute_process( |
| 78 | + COMMAND ${GIT_EXECUTABLE} rev-list --count HEAD |
| 79 | + WORKING_DIRECTORY ${PROJECT_VERSION_DIR} |
| 80 | + OUTPUT_VARIABLE _MASTER_COMMIT_COUNT |
| 81 | + OUTPUT_STRIP_TRAILING_WHITESPACE |
| 82 | + ERROR_QUIET |
| 83 | + ) |
| 84 | + endif() |
| 85 | + if("${_MASTER_COMMIT_COUNT}" STREQUAL "") |
| 86 | + set(_MASTER_COMMIT_COUNT 0) |
| 87 | + endif() |
| 88 | + |
| 89 | + # Count commits ahead of the default branch on current branch. |
| 90 | + # Will be 0 when building on the default branch itself. |
| 91 | + if(NOT "${_DEFAULT_BRANCH}" STREQUAL "") |
| 92 | + execute_process( |
| 93 | + COMMAND ${GIT_EXECUTABLE} rev-list --count ${_DEFAULT_BRANCH}..HEAD |
| 94 | + WORKING_DIRECTORY ${PROJECT_VERSION_DIR} |
| 95 | + OUTPUT_VARIABLE _BRANCH_COMMIT_COUNT |
| 96 | + OUTPUT_STRIP_TRAILING_WHITESPACE |
| 97 | + ERROR_QUIET |
| 98 | + ) |
| 99 | + endif() |
| 100 | + |
| 101 | + # Fallback: if no default branch found or command fails, use 0. |
| 102 | + if("${_BRANCH_COMMIT_COUNT}" STREQUAL "") |
| 103 | + set(_BRANCH_COMMIT_COUNT 0) |
| 104 | + endif() |
| 105 | + |
| 106 | + # --- Commit hash --- |
| 107 | + execute_process( |
| 108 | + COMMAND ${GIT_EXECUTABLE} rev-parse --short HEAD |
| 109 | + WORKING_DIRECTORY ${PROJECT_VERSION_DIR} |
| 110 | + OUTPUT_VARIABLE _GIT_HASH |
| 111 | + OUTPUT_STRIP_TRAILING_WHITESPACE |
| 112 | + ) |
| 113 | + |
| 114 | + # --- Branch name --- |
| 115 | + execute_process( |
| 116 | + COMMAND ${GIT_EXECUTABLE} rev-parse --abbrev-ref HEAD |
| 117 | + WORKING_DIRECTORY ${PROJECT_VERSION_DIR} |
| 118 | + OUTPUT_VARIABLE _GIT_BRANCH |
| 119 | + OUTPUT_STRIP_TRAILING_WHITESPACE |
| 120 | + ) |
| 121 | + |
| 122 | + # --- Dirty status --- |
| 123 | + execute_process( |
| 124 | + COMMAND ${GIT_EXECUTABLE} diff --quiet HEAD |
| 125 | + WORKING_DIRECTORY ${PROJECT_VERSION_DIR} |
| 126 | + RESULT_VARIABLE GIT_DIRTY_RC |
| 127 | + ) |
| 128 | + if(GIT_DIRTY_RC EQUAL 0) |
| 129 | + set(_GIT_DIRTY 0) |
| 130 | + else() |
| 131 | + set(_GIT_DIRTY 1) |
| 132 | + endif() |
| 133 | +else() |
| 134 | + # Fallback when git is not available. |
| 135 | + set(_MASTER_COMMIT_COUNT 0) |
| 136 | + set(_BRANCH_COMMIT_COUNT 0) |
| 137 | + set(_GIT_HASH "unknown") |
| 138 | + set(_GIT_BRANCH "unknown") |
| 139 | + set(_GIT_DIRTY 0) |
| 140 | +endif() |
| 141 | + |
| 142 | +# --- Detached HEAD resolution --- |
| 143 | +# Submodules are typically checked out in detached HEAD state. |
| 144 | +# Try multiple strategies to resolve the actual branch name. |
| 145 | +if("${_GIT_BRANCH}" STREQUAL "HEAD") |
| 146 | + # Strategy 1: Check if HEAD matches a known default branch exactly. |
| 147 | + # This is the most common case for submodules pinned to master/main. |
| 148 | + if(NOT "${_DEFAULT_BRANCH}" STREQUAL "") |
| 149 | + execute_process( |
| 150 | + COMMAND ${GIT_EXECUTABLE} rev-parse HEAD |
| 151 | + WORKING_DIRECTORY ${PROJECT_VERSION_DIR} |
| 152 | + OUTPUT_VARIABLE _HEAD_SHA |
| 153 | + OUTPUT_STRIP_TRAILING_WHITESPACE |
| 154 | + ERROR_QUIET |
| 155 | + ) |
| 156 | + execute_process( |
| 157 | + COMMAND ${GIT_EXECUTABLE} rev-parse ${_DEFAULT_BRANCH} |
| 158 | + WORKING_DIRECTORY ${PROJECT_VERSION_DIR} |
| 159 | + OUTPUT_VARIABLE _DEFAULT_BRANCH_SHA |
| 160 | + OUTPUT_STRIP_TRAILING_WHITESPACE |
| 161 | + ERROR_QUIET |
| 162 | + ) |
| 163 | + if("${_HEAD_SHA}" STREQUAL "${_DEFAULT_BRANCH_SHA}") |
| 164 | + string(REGEX REPLACE "^origin/" "" _GIT_BRANCH "${_DEFAULT_BRANCH}") |
| 165 | + endif() |
| 166 | + endif() |
| 167 | + |
| 168 | + # Strategy 2: Try remote tracking branches containing this commit. |
| 169 | + if("${_GIT_BRANCH}" STREQUAL "HEAD") |
| 170 | + execute_process( |
| 171 | + COMMAND ${GIT_EXECUTABLE} branch -r --contains HEAD |
| 172 | + WORKING_DIRECTORY ${PROJECT_VERSION_DIR} |
| 173 | + OUTPUT_VARIABLE _REMOTE_BRANCHES |
| 174 | + OUTPUT_STRIP_TRAILING_WHITESPACE |
| 175 | + ERROR_QUIET |
| 176 | + ) |
| 177 | + if(NOT "${_REMOTE_BRANCHES}" STREQUAL "") |
| 178 | + # Filter out "HEAD ->" entries and take the first real branch. |
| 179 | + string(REGEX REPLACE "[^\n]*HEAD -> [^\n]*\n?" "" _REMOTE_BRANCHES "${_REMOTE_BRANCHES}") |
| 180 | + string(STRIP "${_REMOTE_BRANCHES}" _REMOTE_BRANCHES) |
| 181 | + if(NOT "${_REMOTE_BRANCHES}" STREQUAL "") |
| 182 | + string(REGEX MATCH "[^ \n]+" _GIT_BRANCH "${_REMOTE_BRANCHES}") |
| 183 | + string(REGEX REPLACE "^origin/" "" _GIT_BRANCH "${_GIT_BRANCH}") |
| 184 | + endif() |
| 185 | + endif() |
| 186 | + endif() |
| 187 | + |
| 188 | + # Strategy 3: Try git describe --all for tag or branch reference. |
| 189 | + if("${_GIT_BRANCH}" STREQUAL "HEAD") |
| 190 | + execute_process( |
| 191 | + COMMAND ${GIT_EXECUTABLE} describe --all --always HEAD |
| 192 | + WORKING_DIRECTORY ${PROJECT_VERSION_DIR} |
| 193 | + OUTPUT_VARIABLE _DESCRIBE_OUTPUT |
| 194 | + OUTPUT_STRIP_TRAILING_WHITESPACE |
| 195 | + ERROR_QUIET |
| 196 | + ) |
| 197 | + if(NOT "${_DESCRIBE_OUTPUT}" STREQUAL "") |
| 198 | + string(REGEX REPLACE "^(heads|remotes/origin)/" "" _GIT_BRANCH "${_DESCRIBE_OUTPUT}") |
| 199 | + endif() |
| 200 | + endif() |
| 201 | + |
| 202 | + # Final fallback. |
| 203 | + if("${_GIT_BRANCH}" STREQUAL "HEAD" OR "${_GIT_BRANCH}" STREQUAL "") |
| 204 | + set(_GIT_BRANCH "detached") |
| 205 | + endif() |
| 206 | +endif() |
| 207 | + |
| 208 | +# --- Build timestamp --- |
| 209 | +string(TIMESTAMP _BUILD_TIMESTAMP \"%Y%m%d%H%M%S\") |
| 210 | +
|
| 211 | +# --- Propagate into generic template variables --- |
| 212 | +# The generic ProjectVersion.h.in uses @PROJECT_VERSION_PREFIX@ for the macro prefix |
| 213 | +# and @PROJECT_VERSION_PREFIX_*@ for each value. configure_file replaces these as |
| 214 | +# flat string substitutions. |
| 215 | +set(PROJECT_VERSION_PREFIX_VERSION_MAJOR ${PROJECT_VERSION_MAJOR}) |
| 216 | +set(PROJECT_VERSION_PREFIX_VERSION_MINOR ${PROJECT_VERSION_MINOR}) |
| 217 | +set(PROJECT_VERSION_PREFIX_VERSION_PATCH ${PROJECT_VERSION_PATCH}) |
| 218 | +set(PROJECT_VERSION_PREFIX_MASTER_COMMIT_COUNT ${_MASTER_COMMIT_COUNT}) |
| 219 | +set(PROJECT_VERSION_PREFIX_BRANCH_COMMIT_COUNT ${_BRANCH_COMMIT_COUNT}) |
| 220 | +set(PROJECT_VERSION_PREFIX_GIT_HASH ${_GIT_HASH}) |
| 221 | +set(PROJECT_VERSION_PREFIX_GIT_BRANCH ${_GIT_BRANCH}) |
| 222 | +set(PROJECT_VERSION_PREFIX_GIT_DIRTY ${_GIT_DIRTY}) |
| 223 | +set(PROJECT_VERSION_PREFIX_BUILD_TIMESTAMP ${_BUILD_TIMESTAMP}) |
| 224 | +
|
| 225 | +# Resolve default branch name without the "origin/" prefix. |
| 226 | +if(NOT "${_DEFAULT_BRANCH}" STREQUAL "") |
| 227 | + string(REGEX REPLACE "^origin/" "" _DEFAULT_BRANCH_NAME "${_DEFAULT_BRANCH}") |
| 228 | +else() |
| 229 | + set(_DEFAULT_BRANCH_NAME "master") |
| 230 | +endif() |
| 231 | +set(PROJECT_VERSION_PREFIX_DEFAULT_BRANCH ${_DEFAULT_BRANCH_NAME}) |
| 232 | +
|
| 233 | +# --- Determine template location --- |
| 234 | +# Use PROJECT_VERSION_TEMPLATE_DIR if set, otherwise fall back to the |
| 235 | +# directory containing this script (i.e. the VersionInfo folder). |
| 236 | +if(NOT DEFINED PROJECT_VERSION_TEMPLATE_DIR) |
| 237 | + set(PROJECT_VERSION_TEMPLATE_DIR ${CMAKE_CURRENT_LIST_DIR}) |
| 238 | +endif() |
| 239 | +
|
| 240 | +# --- Generate header --- |
| 241 | +# Uses the single generic template, outputs a project-specific header |
| 242 | +# in the project's source directory. |
| 243 | +configure_file( |
| 244 | + ${PROJECT_VERSION_TEMPLATE_DIR}/ProjectVersion.h.in |
| 245 | + ${PROJECT_VERSION_DIR}/${PROJECT_VERSION_PREFIX}Version.h |
| 246 | + @ONLY |
| 247 | +) |
0 commit comments