23、配置文件生成:configure_file(),生成config.h,版本号与构建时间注入
说实话,很多C++项目里那个 config.h 文件,我见过太多人手动维护了。版本号改一下,两边同步;构建时间忘了更新,debug半天发现是旧版本。嗯,这活儿其实可以交给CMake自动干。
今天我们就聊聊 configure_file() 这个命令。它就像CMake里的模板引擎,能把 .h.in 文件里的占位符,替换成你定义的真实值。说白了,就是「填表」。
23.1 configure_file() 的基本用法
先看一个最简单的例子。假设你有一个 config.h.in 模板文件:
// config.h.in
#pragma once
#define PROJECT_VERSION "@PROJECT_VERSION@"
#define PROJECT_NAME "@PROJECT_NAME@"
然后在 CMakeLists.txt 里这样写:
cmake_minimum_required(VERSION 3.16)
project(MyApp VERSION 1.2.3)
configure_file(
config.h.in
${CMAKE_CURRENT_BINARY_DIR}/config.h
)
CMake 会把 @PROJECT_VERSION@ 替换成 1.2.3,把 @PROJECT_NAME@ 替换成 MyApp。生成的文件在 build 目录下,不会污染源码树。
${CMAKE_CURRENT_BINARY_DIR} 下,然后用 target_include_directories() 把那个目录加进来。这样源码目录干干净净,git 也不会误提交生成文件。
23.2 两种占位符风格
CMake 支持两种占位符写法:
| 风格 | 示例 | 说明 |
|---|---|---|
@VAR@ |
@PROJECT_VERSION@ |
精确替换,只替换完全匹配的变量名 |
${VAR} |
${PROJECT_VERSION} |
宽松替换,可能会与CMake语法冲突 |
我个人建议用 @VAR@ 风格。为什么?因为 ${VAR} 在 CMake 里本身就有特殊含义,万一你在模板里写了 ${SOME_PATH} 这种字符串,CMake 可能会把它当成变量展开,结果就乱了。用 @VAR@ 就安全得多。
23.3 注入版本号与构建时间
好了,重点来了。我们怎么把版本号和构建时间自动注入到 config.h 里?
先写模板文件 config.h.in:
// config.h.in
#pragma once
// 版本信息
#define APP_VERSION_MAJOR @PROJECT_VERSION_MAJOR@
#define APP_VERSION_MINOR @PROJECT_VERSION_MINOR@
#define APP_VERSION_PATCH @PROJECT_VERSION_PATCH@
#define APP_VERSION_STR "@PROJECT_VERSION@"
// 构建信息
#define BUILD_TIMESTAMP "@BUILD_TIMESTAMP@"
#define GIT_COMMIT_HASH "@GIT_COMMIT_HASH@"
然后在 CMakeLists.txt 里:
cmake_minimum_required(VERSION 3.16)
project(MyApp VERSION 2.1.0)
# 获取构建时间戳
string(TIMESTAMP BUILD_TIMESTAMP "%Y-%m-%d %H:%M:%S" UTC)
# 获取 Git commit hash(如果存在 .git 目录)
execute_process(
COMMAND git log -1 --format=%H
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
OUTPUT_VARIABLE GIT_COMMIT_HASH
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET
)
# 如果获取失败,给个默认值
if(NOT GIT_COMMIT_HASH)
set(GIT_COMMIT_HASH "unknown")
endif()
configure_file(
config.h.in
${CMAKE_CURRENT_BINARY_DIR}/config.h
@ONLY # 只替换 @VAR@ 风格的占位符
)
@ONLY 参数告诉 CMake 只替换 @VAR@ 风格的占位符,忽略 ${VAR}。这能避免很多意外替换。
生成出来的 config.h 大概长这样:
// config.h
#pragma once
#define APP_VERSION_MAJOR 2
#define APP_VERSION_MINOR 1
#define APP_VERSION_PATCH 0
#define APP_VERSION_STR "2.1.0"
#define BUILD_TIMESTAMP "2025-03-15 14:32:08"
#define GIT_COMMIT_HASH "a1b2c3d4e5f6..."
23.4 避坑指南
#define PATH "${SOME_DIR}/data",结果 CMake 把 ${SOME_DIR} 当成变量展开了,生成的文件里路径完全不对。后来我改用 @SOME_DIR@ 并加上 @ONLY,问题就解决了。
还有几个常见问题:
- 路径问题: 模板文件路径建议用绝对路径或
${CMAKE_CURRENT_SOURCE_DIR}拼接,别用相对路径,否则不同构建目录下容易找不到文件。 - 重新配置:
configure_file()只在 CMake 重新配置时执行。如果你改了模板文件,记得重新运行cmake或cmake --build时加--reconfigure。 - 字符串引号: 模板里如果值是字符串,记得在模板里加引号,比如
"@PROJECT_VERSION@",否则生成出来会缺少引号,编译报错。
23.5 知识体系图
下面这张图帮你理清 configure_file() 的核心流程:
23.6 实战:完整的版本信息模块
最后,我分享一个我在实际项目中用过的完整方案。它把版本号、构建时间、Git信息全部注入,C++代码里可以直接打印。
CMakeLists.txt:
cmake_minimum_required(VERSION 3.16)
project(VersionDemo VERSION 3.0.1)
# 构建时间
string(TIMESTAMP BUILD_TIMESTAMP "%Y-%m-%d %H:%M:%S" UTC)
# Git 信息
execute_process(
COMMAND git describe --tags --always --dirty
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
OUTPUT_VARIABLE GIT_DESCRIBE
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET
)
if(NOT GIT_DESCRIBE)
set(GIT_DESCRIBE "unknown")
endif()
configure_file(
${CMAKE_CURRENT_SOURCE_DIR}/version.h.in
${CMAKE_CURRENT_BINARY_DIR}/version.h
@ONLY
)
add_executable(demo main.cpp)
target_include_directories(demo PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
version.h.in:
#pragma once
#define VERSION_MAJOR @PROJECT_VERSION_MAJOR@
#define VERSION_MINOR @PROJECT_VERSION_MINOR@
#define VERSION_PATCH @PROJECT_VERSION_PATCH@
#define VERSION_STR "@PROJECT_VERSION@"
#define BUILD_TIME "@BUILD_TIMESTAMP@"
#define GIT_INFO "@GIT_DESCRIBE@"
main.cpp:
#include <iostream>
#include "version.h"
int main() {
std::cout << "Version: " << VERSION_STR << "\n";
std::cout << "Build: " << BUILD_TIME << "\n";
std::cout << "Git: " << GIT_INFO << "\n";
return 0;
}
运行结果:
Version: 3.0.1
Build: 2025-03-15 14:32:08
Git: v3.0.1-5-g3a4b2c1-dirty
嗯,configure_file() 其实就这么简单。它不花哨,但很实用。你想想看,一个自动生成的 config.h 能帮你省掉多少手动同步的麻烦?