版本管理:如何在CMake中管理项目版本号,并通过版本文件(Version.cmake)实现语义化版本控制
版本号这东西,说白了就是项目的身份证。我见过不少团队,版本号随便写,今天v1.0,明天v2.0,后天又回到v1.1,乱得跟一锅粥似的。其实CMake对版本管理有非常优雅的支持,只是很多人没用好。
今天我们就聊聊,怎么在CMake里把版本号管得明明白白。
为什么需要专门的版本管理?
你可能会想:「版本号不就是改个数字吗?直接在CMakeLists.txt里写死不就完了?」
嗯,我以前也这么干过。直到有一次,我维护的一个库被三个项目同时引用,每个项目都要求不同的版本策略。有的要major.minor.patch,有的要带预发布标签,还有的要根据git commit自动生成build号。直接在CMakeLists.txt里写死?那简直是噩梦。
版本管理的核心痛点有三个:
- 一致性:版本号在源码、文档、安装包、API头文件里都要保持一致
- 可追溯:看到版本号就能知道对应哪个git commit、哪个release
- 自动化:发布新版本时,改一个地方,所有地方都跟着变
CMake的project()命令虽然支持设置版本号,但功能有限。真正专业的做法,是把版本管理独立成一个模块文件——Version.cmake。
语义化版本控制(SemVer)速览
语义化版本,说白了就是MAJOR.MINOR.PATCH三段式。我简单解释一下:
| 段位 | 含义 | 什么时候改 |
|---|---|---|
| MAJOR | 主版本号 | 做了不兼容的API修改 |
| MINOR | 次版本号 | 新增了向下兼容的功能 |
| PATCH | 修订号 | 做了向下兼容的问题修复 |
除此之外,还可以加预发布标签,比如1.0.0-alpha.1、2.3.0-rc.2。这些标签表示版本还没正式发布,但已经可以测试了。
我的习惯:在开发阶段,版本号用0.x.y,表示API还不稳定。等第一个稳定版本出来,才跳到1.0.0。这样用户一看版本号,就知道你的项目成熟度。
设计Version.cmake文件
我个人习惯把版本信息单独放在cmake/Version.cmake里。这样做的好处是:不管项目根目录的CMakeLists.txt多复杂,版本管理始终是独立的、可复用的。
下面是我常用的模板:
# cmake/Version.cmake
# 语义化版本控制模块
# 主版本号 - 不兼容API修改时递增
set(PROJECT_VERSION_MAJOR 2)
# 次版本号 - 新增向下兼容功能时递增
set(PROJECT_VERSION_MINOR 1)
# 修订号 - 向下兼容的问题修复时递增
set(PROJECT_VERSION_PATCH 3)
# 预发布标签(可选):alpha, beta, rc.1, rc.2 等
# 正式发布时设为空字符串
set(PROJECT_VERSION_TWEAK "rc.1")
# 构建元数据(可选):通常从git获取
# 如果没有git,可以用时间戳
set(PROJECT_VERSION_BUILD "")
# 计算完整版本号
if(PROJECT_VERSION_TWEAK)
set(PROJECT_VERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}-${PROJECT_VERSION_TWEAK}")
else()
set(PROJECT_VERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}")
endif()
# 生成用于头文件的版本宏
set(PROJECT_VERSION_HEADER "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}")
# 输出版本信息(方便调试)
message(STATUS "Project version: ${PROJECT_VERSION}")
关键点:我把PROJECT_VERSION_TWEAK和PROJECT_VERSION_BUILD分开处理。前者是预发布标签,属于版本号的一部分;后者是构建元数据,不影响版本比较。这样设计,语义化版本的工具(比如npm的semver库)就能正确识别。
在CMakeLists.txt中集成版本文件
有了Version.cmake,集成就很简单了:
# CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
# 先包含版本文件
include(cmake/Version.cmake)
# 使用版本信息定义项目
project(MyAwesomeLib
VERSION ${PROJECT_VERSION}
DESCRIPTION "An awesome library with semantic versioning"
LANGUAGES CXX
)
# 设置导出头文件的版本
set_target_properties(MyAwesomeLib PROPERTIES
VERSION ${PROJECT_VERSION}
SOVERSION ${PROJECT_VERSION_MAJOR}
)
# 生成版本头文件
configure_file(
${CMAKE_CURRENT_SOURCE_DIR}/include/version.h.in
${CMAKE_CURRENT_BINARY_DIR}/include/version.h
@ONLY
)
注意看,我用了configure_file来生成版本头文件。这样C++代码里也能拿到版本号。
生成版本头文件
在include/version.h.in里,我这样写:
#ifndef MYAWESOMELIB_VERSION_H
#define MYAWESOMELIB_VERSION_H
// 自动生成,不要手动修改
#define MYAWESOMELIB_VERSION_MAJOR @PROJECT_VERSION_MAJOR@
#define MYAWESOMELIB_VERSION_MINOR @PROJECT_VERSION_MINOR@
#define MYAWESOMELIB_VERSION_PATCH @PROJECT_VERSION_PATCH@
#define MYAWESOMELIB_VERSION_TWEAK "@PROJECT_VERSION_TWEAK@"
#define MYAWESOMELIB_VERSION "@PROJECT_VERSION@"
#define MYAWESOMELIB_VERSION_HEADER "@PROJECT_VERSION_HEADER@"
#endif // MYAWESOMELIB_VERSION_H
CMake在配置阶段会把@VAR@替换成实际值。这样C++代码里就能直接使用这些宏了:
#include "version.h"
#include <iostream>
int main() {
std::cout << "Version: " << MYAWESOMELIB_VERSION << std::endl;
std::cout << "Major: " << MYAWESOMELIB_VERSION_MAJOR << std::endl;
return 0;
}
自动从Git获取构建元数据
我曾经踩过一个坑:发布了一个版本,但后来发现代码和版本号对不上。从那以后,我坚持在版本里嵌入git信息。
在Version.cmake里加一段逻辑:
# 尝试从git获取提交信息
find_package(Git QUIET)
if(Git_FOUND)
execute_process(
COMMAND ${GIT_EXECUTABLE} rev-parse --short HEAD
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
OUTPUT_VARIABLE GIT_COMMIT_HASH
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET
)
if(GIT_COMMIT_HASH)
set(PROJECT_VERSION_BUILD "${GIT_COMMIT_HASH}")
endif()
endif()
# 如果没有git,用时间戳
if(NOT PROJECT_VERSION_BUILD)
string(TIMESTAMP PROJECT_VERSION_BUILD "%Y%m%d%H%M%S")
endif()
# 完整版本号(含构建元数据)
set(PROJECT_VERSION_FULL "${PROJECT_VERSION}+${PROJECT_VERSION_BUILD}")
message(STATUS "Full version: ${PROJECT_VERSION_FULL}")
注意:构建元数据(+后面的部分)在语义化版本中不参与版本比较。1.0.0+abc123和1.0.0+def456被认为是同一个版本。它只是用来追溯构建来源的。
版本比较与依赖管理
如果你的库被其他项目依赖,版本比较就很重要了。CMake提供了if(VERSION_xxx)命令:
# 在依赖方检查版本
if(PROJECT_VERSION VERSION_LESS "2.0.0")
message(FATAL_ERROR "This project requires MyAwesomeLib >= 2.0.0")
endif()
# 或者更精确的检查
if(PROJECT_VERSION_MAJOR EQUAL 1)
message(WARNING "Version 1.x is deprecated, please upgrade to 2.x")
endif()
但要注意,VERSION_LESS这类比较只支持MAJOR.MINOR.PATCH三段式。如果你带了预发布标签(比如2.1.3-rc.1),比较结果可能不符合预期。我一般会在正式发布前去掉预发布标签。
版本发布工作流
最后,分享一个我常用的版本发布流程:
- 开发阶段:版本号带
-dev或-alpha标签,比如2.2.0-dev - 功能冻结:创建release分支,版本号改为
2.2.0-rc.1 - 测试修复:递增rc编号,
2.2.0-rc.2、2.2.0-rc.3 - 正式发布:去掉标签,
2.2.0,打git tag - hotfix:从tag创建分支,递增PATCH,
2.2.1
这个流程配合Version.cmake,每次发布只需要改一个文件里的几个数字,然后执行:
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
cmake --build .
cpack
版本号会自动嵌入到所有需要的地方。
避坑指南:我曾经在Version.cmake里直接用了project()命令,结果导致循环包含。记住,Version.cmake只负责定义变量,不要在里面调用project()或add_executable()。它就是个纯数据文件。
知识体系总览
下面这张图总结了版本管理的核心逻辑:
从图上可以看出,Version.cmake是整个版本管理的枢纽。它接收手动定义的语义化版本和自动获取的git信息,组合成完整的版本号,然后分发给CMake配置和C++头文件。这样无论从哪个角度看,版本号都是一致的。
好了,版本管理这块就聊到这儿。记住一句话:版本号不是随便写的数字,它是你和用户之间的契约。用Version.cmake管好它,你的项目会显得更专业,用户用起来也更放心。