一、核心需求与版本规则复盘

在动手前先明确核心目标,避免版本管理混乱:

  • 版本格式:主版本.MINOR.补丁版本(语义化规范,如 2.3.15)

  • 规则 1:主版本 / 小版本(MINOR)手动更新时,补丁版本重置为当前 Git 提交数

  • 规则 2:无主 / 小版本变更时,补丁版本自动跟随 Git 提交数递增

  • 规则 3:版本号需嵌入代码(如 version.h)、构建产物(如二进制文件名)、CI/CD 流程

优势:无需手动维护补丁版本,Git 提交记录即版本追溯依据,避免重复或遗漏。

二、CMake 实现方案(零外部依赖)

核心思路:用 CMake 内置命令调用 Git 获取提交数,结合手动配置的主 / 小版本,自动生成完整版本号,并同步到代码和构建流程。

1. 完整 CMake 脚本(version.cmake)

创建独立的 version.cmake 文件(便于复用),放入工程根目录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# ==============================================================================
# 基于 Git 提交数的版本管理脚本(CMake 3.12+ 兼容)
# 用法:在根 CMakeLists.txt 中 include(version.cmake)
# ==============================================================================

# -------------------------- 1. 手动配置:主版本和小版本 --------------------------
set(MAJOR_VERSION 2) # 重大变更时递增(如 1→2)
set(MINOR_VERSION 1) # 兼容性功能新增时递增(如 1→2)

# -------------------------- 2. 自动获取:Git 提交数(补丁版本) --------------------------
# 初始化补丁版本为 0(无 Git 仓库时 fallback)
set(PATCH_VERSION 0)

# 检查是否为 Git 仓库,且存在 git 命令
find_package(Git QUIET)
if(GIT_FOUND AND EXISTS "${CMAKE_SOURCE_DIR}/.git")
# 统计当前分支的有效提交数(排除合并提交、空提交)
execute_process(
COMMAND ${GIT_EXECUTABLE} rev-list --count --no-merges HEAD
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
OUTPUT_VARIABLE GIT_COMMIT_COUNT
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET # 忽略 Git 命令执行失败(如无提交记录)
)

# 转换为整数(确保非空)
if(GIT_COMMIT_COUNT MATCHES "^[0-9]+$")
set(PATCH_VERSION ${GIT_COMMIT_COUNT})
endif()
endif()

# -------------------------- 3. 生成完整版本号 --------------------------
# 完整版本字符串(如 "2.1.15")
set(FULL_VERSION "${MAJOR_VERSION}.${MINOR_VERSION}.${PATCH_VERSION}")
# 版本号整数(用于代码宏定义,如 20115 = 2*10000 + 1*100 +15)
math(EXPR VERSION_INT "${MAJOR_VERSION}*10000 + ${MINOR_VERSION}*100 + ${PATCH_VERSION}")

# -------------------------- 4. 暴露版本信息到工程 --------------------------
# 1) 给 CMake 其他模块使用(如根 CMakeLists.txt)
set(PROJECT_VERSION ${FULL_VERSION} CACHE INTERNAL "Project full version")
set(PROJECT_VERSION_MAJOR ${MAJOR_VERSION} CACHE INTERNAL "Project major version")
set(PROJECT_VERSION_MINOR ${MINOR_VERSION} CACHE INTERNAL "Project minor version")
set(PROJECT_VERSION_PATCH ${PATCH_VERSION} CACHE INTERNAL "Project patch version")

# 2) 打印版本信息(构建时控制台输出)
message(STATUS "========================================")
message(STATUS "Project Version: ${FULL_VERSION} (INT: ${VERSION_INT})")
message(STATUS " - Major: ${MAJOR_VERSION}")
message(STATUS " - Minor: ${MINOR_VERSION}")
message(STATUS " - Patch: ${PATCH_VERSION} (Git commit count)")
message(STATUS "========================================")

# -------------------------- 5. 生成版本头文件(供代码调用) --------------------------
# 定义头文件模板(会替换 {PLACEHOLDER} 为实际版本值)
set(VERSION_HEADER_CONTENT "
#ifndef PROJECT_VERSION_H
#define PROJECT_VERSION_H

// 版本号宏定义(语义化拆分)
#define PROJECT_VERSION_MAJOR ${MAJOR_VERSION}
#define PROJECT_VERSION_MINOR ${MINOR_VERSION}
#define PROJECT_VERSION_PATCH ${PATCH_VERSION}

// 完整版本字符串(如 \"2.1.15\")
#define PROJECT_FULL_VERSION \"${FULL_VERSION}\"

// 版本号整数(用于版本比较,如 20115 > 20000)
#define PROJECT_VERSION_INT ${VERSION_INT}

#endif // PROJECT_VERSION_H
")

# 生成头文件到构建目录(避免污染源码)
file(WRITE "${CMAKE_BINARY_DIR}/generated/version.h" "${VERSION_HEADER_CONTENT}")

# 让工程能 include 该头文件(无需手动复制)
include_directories("${CMAKE_BINARY_DIR}/generated")

2. 根目录 CMakeLists.txt 集成

在工程根 CMakeLists.txt 中引入上述脚本,核心步骤:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 最低 CMake 版本要求(需支持 execute_process 稳定调用 Git)
cmake_minimum_required(VERSION 3.12)

# 项目名称(版本号会被脚本覆盖,此处可留空)
project(MyProject LANGUAGES CXX)

# -------------------------- 引入版本管理脚本 --------------------------
include(version.cmake)

# -------------------------- 后续工程配置(示例) --------------------------
# 1. 配置可执行文件,嵌入版本号到文件名
add_executable(MyApp src/main.cpp)
set_target_properties(MyApp PROPERTIES
OUTPUT_NAME "MyApp-v${FULL_VERSION}" # 输出文件名:MyApp-v2.1.15
CXX_STANDARD 17
CXX_STANDARD_REQUIRED ON
)

# 2. 传递版本号到编译选项(可选,如给编译器定义)
target_compile_definitions(MyApp PRIVATE
PROJECT_FULL_VERSION="${FULL_VERSION}"
)

# 3. 安装时带上版本信息(可选)
install(TARGETS MyApp
RUNTIME DESTINATION bin
CONFIGURATIONS Release
)
install(FILES "${CMAKE_BINARY_DIR}/generated/version.h"
DESTINATION include/MyProject
)

三、代码中使用版本号

生成的 version.h 会自动放入构建目录的 generated 文件夹,代码中直接引用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/main.cpp
#include <iostream>
#include "version.h" // 无需手动复制,CMake 已配置 include 路径

int main() {
std::cout << "========================================" << std::endl;
std::cout << "MyApp Version: " << PROJECT_FULL_VERSION << std::endl;
std::cout << "Version Int: " << PROJECT_VERSION_INT << std::endl;
std::cout << "========================================" << std::endl;

// 版本比较示例(如仅支持 2.0.0 以上版本的功能)
if (PROJECT_VERSION_INT >= 20000) {
std::cout << "支持高级功能(版本 >= 2.0.0)" << std::endl;
} else {
std::cout << "基础功能模式" << std::endl;
}
return 0;
}

四、关键操作指南

1. 如何更新主版本 / 小版本?

直接修改 version.cmake 中的 MAJOR_VERSION 或 MINOR_VERSION:

  • 主版本更新(如 2.1.x → 3.0.x):set(MAJOR_VERSION 3) + set(MINOR_VERSION 0)

  • 小版本更新(如 2.1.x → 2.2.x):set(MINOR_VERSION 2)

  • 效果:补丁版本自动重置为当前 Git 提交数(无需手动改 PATCH_VERSION)

2. 构建产物示例

  • 可执行文件:MyApp-v2.1.15.exe(Windows)/ MyApp-v2.1.15(Linux/Mac)

  • 头文件:build/generated/version.h(编译时自动引用)

  • 控制台输出(构建时):

1
2
3
4
5
6
========================================
Project Version: 2.1.15 (INT: 20115)
- Major: 2
- Minor: 1
- Patch: 15 (Git commit count)
========================================

3. 无 Git 仓库场景适配

  • 若工程未初始化 Git(如首次构建),补丁版本默认设为 0(完整版本如 2.1.0)

  • 若 Git 命令执行失败(如无提交记录),同样 fallback 到 PATCH_VERSION=0

总结

本方案通过 CMake 原生命令实现了 “主 / 小版本手动控制、补丁版本自动跟随 Git 提交数” 的核心需求,无需依赖 Python/Shell 脚本,工程集成成本低,且支持代码嵌入、产物命名、CI/CD 等全流程适配。

只需修改 version.cmake 中的主 / 小版本,补丁版本会自动同步 Git 提交记录,既符合语义化版本规范,又减少了手动维护版本号的工作量,适合中小型 C/C++ 工程直接使用。