Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ cmake_install.cmake
install_manifest.txt
compile_commands.json
CTestTestfile.cmake
/build/
_deps
/.vs
/out
213 changes: 118 additions & 95 deletions CMake/Version.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -5,89 +5,102 @@
# @note 3.20 required for `GENERATED` attribute to be project-wide i.e. Version.h isn't build until build-time
cmake_minimum_required(VERSION 3.20)

#TODO? if ( DEFINED VERSION_SEMANTIC )
#return()
#endif()

# TODO? if ( DEFINED VERSION_SEMANTIC )
# return()
# endif()
message(CHECK_START "Version.cmake")
list(APPEND CMAKE_MESSAGE_INDENT " ")

set(VERSION_OUT_DIR "${CMAKE_BINARY_DIR}" CACHE PATH "Destination directory into which `Version.cmake` shall genrate Versioning header files")
if(NOT VERSION_OUT_DIR)
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The guard if(NOT VERSION_OUT_DIR) checks truthiness, so values like ...-NOTFOUND/OFF/0 will be treated as false and silently replaced. Since this is a PATH option, it’s safer to check whether it is defined (and optionally whether it’s an empty string) rather than boolean-evaluating the path value.

Suggested change
if(NOT VERSION_OUT_DIR)
if(NOT DEFINED VERSION_OUT_DIR OR "${VERSION_OUT_DIR}" STREQUAL "")

Copilot uses AI. Check for mistakes.
set(VERSION_OUT_DIR "${CMAKE_BINARY_DIR}" CACHE PATH "Destination directory into which `Version.cmake` shall genrate Versioning header files")
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spelling in the cache description: "shall genrate" should be "shall generate".

Suggested change
set(VERSION_OUT_DIR "${CMAKE_BINARY_DIR}" CACHE PATH "Destination directory into which `Version.cmake` shall genrate Versioning header files")
set(VERSION_OUT_DIR "${CMAKE_BINARY_DIR}" CACHE PATH "Destination directory into which `Version.cmake` shall generate Versioning header files")

Copilot uses AI. Check for mistakes.
endif()

set(VERSION_SOURCE_DIR "${CMAKE_SOURCE_DIR}" CACHE PATH "Repositroy directory used for `Version.cmake` repo versioning")
set(VERSION_PREFIX "" CACHE STRING "Prefix for generated files and definitions")

# Get cmakeVersion information
message(CHECK_START "Find git")
if( NOT DEFINED GIT_EXECUTABLE ) # Find Git or bail out
find_package( Git )
if( NOT Git_FOUND )

if(NOT DEFINED GIT_EXECUTABLE) # Find Git or bail out
find_package(Git)

if(NOT Git_FOUND)
message(CHECK_FAIL "Not found in PATH")
else()
message(CHECK_PASS "Found: '${GIT_EXECUTABLE}'")
endif()
else()
message(CHECK_PASS "Using pre-defined GIT_EXECUTABLE: '${GIT_EXECUTABLE}'")
endif()

# Git describe
# @note Exclude 'tweak' tags in the form v0.1.2-30 i.e. with the '-30' to avoid a second suffix being appended e.g v0.1.2-30-12
set(GIT_VERSION_COMMAND "${GIT_EXECUTABLE}" -C "${VERSION_SOURCE_DIR}" --no-pager describe --tags --exclude "v[0-9]*[._][0-9]*[._][0-9]*-[0-9]*" --always --dirty --long)

# Git count
# @note We only count commits on the current branch and not comits in merge branches via '--first-parent'. The count is never unique but the Sha will be!
set(GIT_COUNT_COMMAND "${GIT_EXECUTABLE}" -C "${VERSION_SOURCE_DIR}" rev-list --count --first-parent HEAD)
set(GIT_COUNT_COMMAND "${GIT_EXECUTABLE}" -C "${VERSION_SOURCE_DIR}" rev-list --count --first-parent HEAD)

# Git cache path
set(GIT_CACHE_PATH_COMMAND "${GIT_EXECUTABLE}" -C "${VERSION_SOURCE_DIR}" rev-parse --git-dir)

# Get current date
string(TIMESTAMP VERSION_DATE "%Y-%m-%d")
string(TIMESTAMP VERSION_DATETIME "%Y-%m-%d %H:%M:%S")

macro(version_parseSemantic semVer)
if( "${semVer}" MATCHES "^v?([0-9]+)[._]([0-9]+)[._]?([0-9]+)?[-]([0-9]+)[-][g]([._0-9A-Fa-f]+)[-]?(dirty)?$")
set( _VERSION_SET TRUE)
math( EXPR _VERSION_MAJOR "${CMAKE_MATCH_1}+0")
math( EXPR _VERSION_MINOR "${CMAKE_MATCH_2}+0")
math( EXPR _VERSION_PATCH "${CMAKE_MATCH_3}+0")
math( EXPR _VERSION_COMMIT "${CMAKE_MATCH_4}+0")
set( _VERSION_SHA "${CMAKE_MATCH_5}")
set( _VERSION_DIRTY "${CMAKE_MATCH_6}")
set( _VERSION_SEMANTIC ${_VERSION_MAJOR}.${_VERSION_MINOR}.${_VERSION_PATCH}.${_VERSION_COMMIT} )
set( _VERSION_FULL ${git_describe} )
if("${VERSION_PREFIX}" STREQUAL "")
set( _VERSION_PREFIX "")
else()
set( _VERSION_PREFIX "${VERSION_PREFIX}_")
endif()
else()
set( _VERSION_SET FALSE)
if("${semVer}" MATCHES "^v?([0-9]+)[._]([0-9]+)[._]?([0-9]+)?[-]([0-9]+)[-][g]([._0-9A-Fa-f]+)[-]?(dirty)?$")
set(_VERSION_SET TRUE)
math(EXPR _VERSION_MAJOR "${CMAKE_MATCH_1}+0")
math(EXPR _VERSION_MINOR "${CMAKE_MATCH_2}+0")
math(EXPR _VERSION_PATCH "${CMAKE_MATCH_3}+0")
math(EXPR _VERSION_COMMIT "${CMAKE_MATCH_4}+0")
set(_VERSION_SHA "${CMAKE_MATCH_5}")
set(_VERSION_DIRTY "${CMAKE_MATCH_6}")
set(_VERSION_SEMANTIC ${_VERSION_MAJOR}.${_VERSION_MINOR}.${_VERSION_PATCH}.${_VERSION_COMMIT})
set(_VERSION_FULL ${git_describe})

if("${VERSION_PREFIX}" STREQUAL "")
set(_VERSION_PREFIX "")
else()
set(_VERSION_PREFIX "${VERSION_PREFIX}_")
endif()
else()
set(_VERSION_SET FALSE)
endif()
endmacro()

macro(version_export_variables)
set( VERSION_SET ${_VERSION_SET} CACHE INTERNAL "" FORCE)
set( VERSION_MAJOR ${_VERSION_MAJOR} CACHE INTERNAL "" FORCE)
set( VERSION_MINOR ${_VERSION_MINOR} CACHE INTERNAL "" FORCE)
set( VERSION_PATCH ${_VERSION_PATCH} CACHE INTERNAL "" FORCE)
set( VERSION_COMMIT ${_VERSION_COMMIT} CACHE INTERNAL "" FORCE)
set( VERSION_SHA ${_VERSION_SHA} CACHE INTERNAL "" FORCE)
set( VERSION_DIRTY ${_VERSION_DIRTY} CACHE INTERNAL "" FORCE)
set( VERSION_SEMANTIC ${_VERSION_SEMANTIC} CACHE INTERNAL "" FORCE)
set( VERSION_FULL ${_VERSION_FULL} CACHE INTERNAL "" FORCE)
set(VERSION_SET ${_VERSION_SET} CACHE INTERNAL "" FORCE)
set(VERSION_MAJOR ${_VERSION_MAJOR} CACHE INTERNAL "" FORCE)
set(VERSION_MINOR ${_VERSION_MINOR} CACHE INTERNAL "" FORCE)
set(VERSION_PATCH ${_VERSION_PATCH} CACHE INTERNAL "" FORCE)
set(VERSION_COMMIT ${_VERSION_COMMIT} CACHE INTERNAL "" FORCE)
set(VERSION_SHA ${_VERSION_SHA} CACHE INTERNAL "" FORCE)
set(VERSION_DIRTY ${_VERSION_DIRTY} CACHE INTERNAL "" FORCE)
set(VERSION_SEMANTIC ${_VERSION_SEMANTIC} CACHE INTERNAL "" FORCE)
set(VERSION_FULL ${_VERSION_FULL} CACHE INTERNAL "" FORCE)
set(VERSION_DATE ${VERSION_DATE} CACHE INTERNAL "" FORCE)
set(VERSION_DATETIME ${VERSION_DATETIME} CACHE INTERNAL "" FORCE)
endmacro()

message(CHECK_START "Git Cache-Path")
if ( DEFINED GIT_CACHE_PATH)

if(DEFINED GIT_CACHE_PATH)
message(CHECK_PASS "Using pre-defined GIT_CACHE_PATH '${GIT_CACHE_PATH}'")
else()
execute_process(
COMMAND ${GIT_CACHE_PATH_COMMAND}
RESULT_VARIABLE _GIT_RESULT
OUTPUT_VARIABLE GIT_CACHE_PATH
ERROR_VARIABLE _GIT_ERROR
COMMAND ${GIT_CACHE_PATH_COMMAND}
RESULT_VARIABLE _GIT_RESULT
OUTPUT_VARIABLE GIT_CACHE_PATH
ERROR_VARIABLE _GIT_ERROR
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_STRIP_TRAILING_WHITESPACE
${capture_output}
)
if( NOT _GIT_RESULT EQUAL 0 )
message( CHECK_FAIL "Failed: ${GIT_CACHE_PATH_COMMAND}\nRESULT_VARIABLE:'${_GIT_RESULT}' \nOUTPUT_VARIABLE:'${GIT_CACHE_PATH}' \nERROR_VARIABLE:'${_GIT_ERROR}'")

if(NOT _GIT_RESULT EQUAL 0)
message(CHECK_FAIL "Failed: ${GIT_CACHE_PATH_COMMAND}\nRESULT_VARIABLE:'${_GIT_RESULT}' \nOUTPUT_VARIABLE:'${GIT_CACHE_PATH}' \nERROR_VARIABLE:'${_GIT_ERROR}'")
else()
file(TO_CMAKE_PATH "${VERSION_SOURCE_DIR}/${GIT_CACHE_PATH}" GIT_CACHE_PATH)
message(CHECK_PASS "Success '${GIT_CACHE_PATH}'")
Expand All @@ -96,48 +109,54 @@ endif()

message(CHECK_START "Git Describe")
execute_process(
COMMAND ${GIT_VERSION_COMMAND}
RESULT_VARIABLE _GIT_RESULT
OUTPUT_VARIABLE git_describe
ERROR_VARIABLE _GIT_ERROR
COMMAND ${GIT_VERSION_COMMAND}
RESULT_VARIABLE _GIT_RESULT
OUTPUT_VARIABLE git_describe
ERROR_VARIABLE _GIT_ERROR
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_STRIP_TRAILING_WHITESPACE
${capture_output}
)
if( NOT _GIT_RESULT EQUAL 0 )
message( CHECK_FAIL "Failed: ${GIT_VERSION_COMMAND}\nResult:'${_GIT_RESULT}' Error:'${_GIT_ERROR}'")
if ( "${_GIT_ERROR}" STREQUAL "fatal: bad revision 'HEAD'")

if(NOT _GIT_RESULT EQUAL 0)
message(CHECK_FAIL "Failed: ${GIT_VERSION_COMMAND}\nResult:'${_GIT_RESULT}' Error:'${_GIT_ERROR}'")

if("${_GIT_ERROR}" STREQUAL "fatal: bad revision 'HEAD'")
set(_VERSION_NOT_GIT_REPO TRUE) # Flag that we don't have a valid git-repository'
endif()
else()
message(CHECK_PASS "Success '${git_describe}'")

message(CHECK_START "Parse version")
version_parseSemantic(${git_describe})
if( ${_VERSION_SET} )

if(${_VERSION_SET})
message(CHECK_PASS "Tag '${git_describe}' is a valid semantic version [${_VERSION_SEMANTIC}]")
message(STATUS "Git Datetime: ${VERSION_DATETIME}")
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

message(STATUS "Git Datetime: ${VERSION_DATETIME}") is misleading: VERSION_DATETIME is the current configure-time timestamp (from string(TIMESTAMP ...)), not a Git-derived datetime. Consider renaming the label (e.g., "Configure datetime"/"Build datetime") or extracting the commit date if you intend it to be Git time.

Suggested change
message(STATUS "Git Datetime: ${VERSION_DATETIME}")
message(STATUS "Configure datetime: ${VERSION_DATETIME}")

Copilot uses AI. Check for mistakes.
else()
message(CHECK_FAIL "'${git_describe}' is not a valid semantic-version e.g. 'v0.1.2-30'")
endif()
endif()

if(NOT DEFINED _VERSION_FULL AND NOT _VERSION_NOT_GIT_REPO)
message(CHECK_START "Fallback as Git-Count")
execute_process(
COMMAND ${GIT_COUNT_COMMAND}
RESULT_VARIABLE _GIT_RESULT
OUTPUT_VARIABLE git_count
ERROR_VARIABLE _GIT_ERROR
COMMAND ${GIT_COUNT_COMMAND}
RESULT_VARIABLE _GIT_RESULT
OUTPUT_VARIABLE git_count
ERROR_VARIABLE _GIT_ERROR
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_STRIP_TRAILING_WHITESPACE
${capture_output}
)
if( NOT _GIT_RESULT EQUAL 0 )
message( CHECK_FAIL "Failed: ${GIT_COUNT_COMMAND}\nResult:'${_GIT_RESULT}' Error:'${_GIT_ERROR}'")
else()

if(NOT _GIT_RESULT EQUAL 0)
message(CHECK_FAIL "Failed: ${GIT_COUNT_COMMAND}\nResult:'${_GIT_RESULT}' Error:'${_GIT_ERROR}'")
else()
set(git_describe "0.0.0-${git_count}-g${git_describe}")
version_parseSemantic(${git_describe})
if( ${VERSION_SET} )

if(${VERSION_SET})
message(CHECK_PASS "git-tag '${git_describe} is a valid semantic version")
else()
message(CHECK_FAIL "'${git_describe}' is not a valid semantic-version e.g. 'v0.1.2-30'")
Expand All @@ -146,94 +165,98 @@ if(NOT DEFINED _VERSION_FULL AND NOT _VERSION_NOT_GIT_REPO)
endif()

function(gitversion_configure_file VERSION_H_TEMPLATE VERSION_H)
configure_file (
configure_file(
"${VERSION_H_TEMPLATE}"
"${VERSION_H}"
)
endfunction()

version_export_variables()

if ( VERSION_GENERATE_NOW )
gitversion_configure_file( ${VERSION_H_TEMPLATE} ${VERSION_H})
else()
if(VERSION_GENERATE_NOW)
gitversion_configure_file(${VERSION_H_TEMPLATE} ${VERSION_H})
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gitversion_configure_file(${VERSION_H_TEMPLATE} ${VERSION_H}) passes unquoted paths; if either path contains spaces/semicolons, argument splitting will break and configure_file() won’t receive the intended filenames. Quote both arguments at the call site.

Suggested change
gitversion_configure_file(${VERSION_H_TEMPLATE} ${VERSION_H})
gitversion_configure_file("${VERSION_H_TEMPLATE}" "${VERSION_H}")

Copilot uses AI. Check for mistakes.
else()
set(VERSION_H_FILENAME "${VERSION_PREFIX}Version.h")
set(VERSION_H_TEMPLATE "${CMAKE_CURRENT_LIST_DIR}/${VERSION_H_FILENAME}.in")
set(VERSION_H "${VERSION_OUT_DIR}/${VERSION_H_FILENAME}")

# If no Version.h.in exists we generate the template with the default
message(CHECK_START "Find '${VERSION_H_FILENAME}.in'")
if ( NOT EXISTS ${VERSION_H_TEMPLATE} )

if(NOT EXISTS ${VERSION_H_TEMPLATE})
set(VERSION_H_TEMPLATE "${VERSION_OUT_DIR}/${VERSION_H_FILENAME}.in")
message( CHECK_FAIL "Not Found. Generating '${VERSION_H_TEMPLATE}'")
message(CHECK_FAIL "Not Found. Generating '${VERSION_H_TEMPLATE}'")

file(WRITE ${VERSION_H_TEMPLATE}
[=[
[=[
Comment on lines +186 to +191
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if(NOT EXISTS ${VERSION_H_TEMPLATE}) / file(WRITE ${VERSION_H_TEMPLATE} ...) use unquoted paths. If the module lives under a directory with spaces (common on Windows), this will mis-parse and can error. Quote ${VERSION_H_TEMPLATE} (and other file paths) anywhere they’re used as single arguments to CMake commands/conditions.

Copilot uses AI. Check for mistakes.
#define @_VERSION_PREFIX@VERSION_MAJOR @_VERSION_MAJOR@
#define @_VERSION_PREFIX@VERSION_MINOR @_VERSION_MINOR@
#define @_VERSION_PREFIX@VERSION_PATCH @_VERSION_PATCH@
#define @_VERSION_PREFIX@VERSION_COMMIT @_VERSION_COMMIT@
#define @_VERSION_PREFIX@VERSION_SHA "@_VERSION_SHA@"
#define @_VERSION_PREFIX@VERSION_SEMANTIC "@_VERSION_SEMANTIC@"
#define @_VERSION_PREFIX@VERSION_FULL "@_VERSION_FULL@"
#define @_VERSION_PREFIX@VERSION_DATE "@VERSION_DATE@"
#define @_VERSION_PREFIX@VERSION_DATETIME "@VERSION_DATETIME@"
]=])
if ( NOT EXISTS ${VERSION_H_TEMPLATE} )
message( FATAL_ERROR "Failed to create template ${VERSION_H_TEMPLATE}")

if(NOT EXISTS ${VERSION_H_TEMPLATE})
message(FATAL_ERROR "Failed to create template ${VERSION_H_TEMPLATE}")
endif()
else()
message( CHECK_PASS "Found '${VERSION_H_TEMPLATE}'")
message(CHECK_PASS "Found '${VERSION_H_TEMPLATE}'")
endif()

# A custom target is used to update Version.h
add_custom_target( genCmakeVersion
add_custom_target(genCmakeVersion
ALL
BYPRODUCTS "${VERSION_H}"
SOURCES "${VERSION_H_TEMPLATE}"
DEPENDS "${GIT_CACHE_PATH}/index"
"${GIT_CACHE_PATH}/HEAD"
"${GIT_CACHE_PATH}/HEAD"
COMMENT "Version.cmake: Generating `${VERSION_H_FILENAME}`"
COMMAND ${CMAKE_COMMAND}
-B "${VERSION_OUT_DIR}"
-D VERSION_GENERATE_NOW=YES
-D VERSION_H_TEMPLATE=${VERSION_H_TEMPLATE}
-D VERSION_H=${VERSION_H}
-D VERSION_PREFIX=${VERSION_PREFIX}
-D GIT_EXECUTABLE=${GIT_EXECUTABLE}
-D CMAKE_MODULE_PATH=${CMAKE_MODULE_PATH}
-P ${CMAKE_CURRENT_LIST_FILE}
WORKING_DIRECTORY ${VERSION_SOURCE_DIR}
COMMAND ${CMAKE_COMMAND}
-B "${VERSION_OUT_DIR}"
-D VERSION_GENERATE_NOW=YES
-D VERSION_H_TEMPLATE=${VERSION_H_TEMPLATE}
-D VERSION_H=${VERSION_H}
-D VERSION_PREFIX=${VERSION_PREFIX}
-D GIT_EXECUTABLE=${GIT_EXECUTABLE}
-D CMAKE_MODULE_PATH=${CMAKE_MODULE_PATH}
-P ${CMAKE_CURRENT_LIST_FILE}
WORKING_DIRECTORY ${VERSION_SOURCE_DIR}
Comment on lines +220 to +227
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the custom target invocation, -D CMAKE_MODULE_PATH=${CMAKE_MODULE_PATH} (and other -D ...=${...} args) are unquoted. If CMAKE_MODULE_PATH contains multiple entries (semicolon-separated) or any value contains spaces, CMake will split the argument list and the script will receive truncated/incorrect -D values. Pass these -D values as a single argument (quote/escape), or serialize list variables explicitly before passing them.

Suggested change
-D VERSION_GENERATE_NOW=YES
-D VERSION_H_TEMPLATE=${VERSION_H_TEMPLATE}
-D VERSION_H=${VERSION_H}
-D VERSION_PREFIX=${VERSION_PREFIX}
-D GIT_EXECUTABLE=${GIT_EXECUTABLE}
-D CMAKE_MODULE_PATH=${CMAKE_MODULE_PATH}
-P ${CMAKE_CURRENT_LIST_FILE}
WORKING_DIRECTORY ${VERSION_SOURCE_DIR}
"-DVERSION_GENERATE_NOW=YES"
"-DVERSION_H_TEMPLATE=${VERSION_H_TEMPLATE}"
"-DVERSION_H=${VERSION_H}"
"-DVERSION_PREFIX=${VERSION_PREFIX}"
"-DGIT_EXECUTABLE=${GIT_EXECUTABLE}"
"-DCMAKE_MODULE_PATH=${CMAKE_MODULE_PATH}"
-P "${CMAKE_CURRENT_LIST_FILE}"
WORKING_DIRECTORY "${VERSION_SOURCE_DIR}"

Copilot uses AI. Check for mistakes.
VERBATIM
)

add_library( cmakeVersion INTERFACE )
add_library(cmakeVersion INTERFACE)
target_include_directories(cmakeVersion INTERFACE "${VERSION_OUT_DIR}")

# @note Explicit file-names - prevent Cmake finding `Version.h.in` for `Version.h`
if (POLICY CMP0115)
if(POLICY CMP0115)
cmake_policy(SET CMP0115 NEW)
endif()

target_sources( cmakeVersion
INTERFACE
"${VERSION_H}")
add_dependencies( cmakeVersion
INTERFACE genCmakeVersion )
add_library( version::version ALIAS cmakeVersion )
target_sources(cmakeVersion
INTERFACE
"${VERSION_H}")
add_dependencies(cmakeVersion
INTERFACE genCmakeVersion)

add_library(version::version ALIAS cmakeVersion)
endif()

list(POP_BACK CMAKE_MESSAGE_INDENT)

if(VERSION_GENERATE_NOW)
set(VERSION_H_GENERATED TRUE)
else()
get_source_file_property(VERSION_H_GENERATED "${VERSION_H}" GENERATED )
get_source_file_property(VERSION_H_GENERATED "${VERSION_H}" GENERATED)
endif()

if ( NOT _VERSION_NOT_GIT_REPO )
if ( NOT VERSION_SET )
if(NOT _VERSION_NOT_GIT_REPO)
if(NOT VERSION_SET)
message(CHECK_FAIL "Version.cmake failed - VERSION_SET==false")
elseif ( ${VERSION_H_GENERATED} )
elseif(${VERSION_H_GENERATED})
message(CHECK_PASS "${VERSION_FULL} [${VERSION_SEMANTIC}] {Generated}")
elseif(EXISTS ${VERSION_H})
message(CHECK_PASS "Using pre-defined '${VERSION_H}'")
Expand Down