26、代码生成:configure_file、模板文件、版本头文件生成、配置头文件
代码生成这事儿,说白了就是让 CMake 在构建前帮你写代码。你想想看,每次改个版本号就要手动改头文件?每次换个平台就要改一堆宏定义?太累了。CMake 的 configure_file 就是干这个的。
26.1 configure_file 的基本用法
configure_file 的作用很简单:读入一个模板文件,把里面的变量替换掉,输出一个新文件。我刚开始用的时候觉得这玩意儿不就是个文本替换吗?后来发现它其实是个「构建时代码生成器」。
基本语法:
configure_file(<input> <output> [COPYONLY] [ESCAPE_QUOTES] [@ONLY])
- input:模板文件路径(通常是 .in 后缀)
- output:生成的目标文件路径
- COPYONLY:只复制,不替换变量
- ESCAPE_QUOTES:转义引号(默认不转义)
- @ONLY:只替换 @VAR@ 形式的变量,不替换 ${VAR}
核心机制:CMake 会扫描模板文件中的 @VAR@ 和 ${VAR} 占位符,用当前 CMake 变量值替换它们。如果变量未定义,替换为空字符串。
26.2 模板文件的设计
模板文件通常以 .h.in 或 .cpp.in 结尾。我个人习惯用 .h.in,因为一眼就能看出这是个模板头文件。
举个最简单的例子。假设我们有个 version.h.in:
#ifndef VERSION_H
#define VERSION_H
#define PROJECT_NAME "@PROJECT_NAME@"
#define PROJECT_VERSION "@PROJECT_VERSION@"
#define PROJECT_VERSION_MAJOR @PROJECT_VERSION_MAJOR@
#define PROJECT_VERSION_MINOR @PROJECT_VERSION_MINOR@
#define PROJECT_VERSION_PATCH @PROJECT_VERSION_PATCH@
#endif // VERSION_H
然后在 CMakeLists.txt 里:
cmake_minimum_required(VERSION 3.16)
project(MyApp VERSION 2.1.3)
configure_file(
"${PROJECT_SOURCE_DIR}/version.h.in"
"${PROJECT_BINARY_DIR}/version.h"
)
构建后生成的 version.h 长这样:
#ifndef VERSION_H
#define VERSION_H
#define PROJECT_NAME "MyApp"
#define PROJECT_VERSION "2.1.3"
#define PROJECT_VERSION_MAJOR 2
#define PROJECT_VERSION_MINOR 1
#define PROJECT_VERSION_PATCH 3
#endif // VERSION_H
小技巧:模板文件中的字符串变量建议用 @VAR@ 形式,数字或宏定义用 ${VAR}。这样一眼就能区分「这是字符串」还是「这是数值」。
26.3 版本头文件生成
版本管理是 configure_file 最经典的应用场景。我在项目中遇到过好几次版本号不一致的问题——代码里写的是 1.0,安装包却是 1.0.1,debug 起来特别痛苦。
更完善的版本头文件模板 version.h.in:
#ifndef VERSION_H
#define VERSION_H
// 自动生成,请勿手动修改
#define APP_NAME "@PROJECT_NAME@"
#define APP_VERSION "@PROJECT_VERSION@"
#define APP_VERSION_MAJOR @PROJECT_VERSION_MAJOR@
#define APP_VERSION_MINOR @PROJECT_VERSION_MINOR@
#define APP_VERSION_PATCH @PROJECT_VERSION_PATCH@
#define APP_BUILD_TIME "@BUILD_TIME@"
#define APP_GIT_HASH "@GIT_HASH@"
// 版本比较宏
#define APP_VERSION_CHECK(major, minor, patch) \
(APP_VERSION_MAJOR > (major) || \
(APP_VERSION_MAJOR == (major) && APP_VERSION_MINOR > (minor)) || \
(APP_VERSION_MAJOR == (major) && APP_VERSION_MINOR == (minor) && APP_VERSION_PATCH >= (patch)))
#endif // VERSION_H
对应的 CMake 配置:
# 获取构建时间
string(TIMESTAMP BUILD_TIME "%Y-%m-%d %H:%M:%S")
# 获取 Git commit hash(如果存在)
find_package(Git QUIET)
if(Git_FOUND)
execute_process(
COMMAND ${GIT_EXECUTABLE} rev-parse --short HEAD
WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}
OUTPUT_VARIABLE GIT_HASH
OUTPUT_STRIP_TRAILING_WHITESPACE
)
else()
set(GIT_HASH "unknown")
endif()
configure_file(
"${PROJECT_SOURCE_DIR}/version.h.in"
"${PROJECT_BINARY_DIR}/version.h"
)
注意:execute_process 在配置阶段执行,不是构建阶段。如果你改了 Git commit 但没有重新 cmake,版本号不会更新。我踩过这个坑——发布时版本号还是旧的,用户反馈 bug 都定位不到正确的 commit。
26.4 配置头文件
配置头文件用来控制「这个功能在哪些平台/条件下启用」。说白了就是编译时的开关。
举个例子,我们有个 config.h.in:
#ifndef CONFIG_H
#define CONFIG_H
// 平台检测
#cmakedefine PLATFORM_WINDOWS
#cmakedefine PLATFORM_LINUX
#cmakedefine PLATFORM_MACOS
// 功能开关
#cmakedefine ENABLE_LOGGING
#cmakedefine ENABLE_SSL
#cmakedefine ENABLE_GPU_ACCELERATION
// 路径配置
#define DATA_DIR "@DATA_DIR@"
#define LOG_DIR "@LOG_DIR@"
// 编译选项
#cmakedefine DEBUG_MODE
#cmakedefine USE_OPENMP
#endif // CONFIG_H
注意这里用的是 #cmakedefine,不是 #define。#cmakedefine 是 CMake 的专有指令,它的行为是:
- 如果 CMake 变量为真(ON/TRUE/1),生成
#define VAR - 如果 CMake 变量为假(OFF/FALSE/0/未定义),生成
/* #undef VAR */
在 CMakeLists.txt 里控制这些开关:
# 平台检测
if(WIN32)
set(PLATFORM_WINDOWS ON)
elseif(APPLE)
set(PLATFORM_MACOS ON)
elseif(UNIX)
set(PLATFORM_LINUX ON)
endif()
# 功能开关
option(ENABLE_LOGGING "Enable logging" ON)
option(ENABLE_SSL "Enable SSL support" OFF)
option(ENABLE_GPU_ACCELERATION "Enable GPU acceleration" OFF)
# 路径配置
set(DATA_DIR "${CMAKE_INSTALL_PREFIX}/share/${PROJECT_NAME}")
set(LOG_DIR "${CMAKE_INSTALL_PREFIX}/var/log/${PROJECT_NAME}")
# 编译选项
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
set(DEBUG_MODE ON)
endif()
find_package(OpenMP)
if(OpenMP_FOUND)
set(USE_OPENMP ON)
endif()
configure_file(
"${PROJECT_SOURCE_DIR}/config.h.in"
"${PROJECT_BINARY_DIR}/config.h"
)
26.5 知识体系图
下面这张图帮你理清 configure_file 的完整工作流:
26.6 避坑指南与最佳实践
用 configure_file 这么多年,我总结了几条经验:
- 输出路径一定要用
${PROJECT_BINARY_DIR}。别把生成的文件放到源码目录里,否则每次 clean 都要手动删。我见过有人直接输出到源码目录,结果 git status 永远有未跟踪文件。 - 模板文件用
.in后缀。这样 IDE 和编辑器能识别,而且一眼就能看出这是模板,不是最终文件。 - 字符串变量记得加引号。在模板里写
@VAR@时,如果 VAR 是字符串,一定要写成"@VAR@",否则生成出来的代码编译会报错。 - 用
#cmakedefine代替#define @VAR@。前者会自动处理 ON/OFF 的语义,后者你得自己写条件判断。 - 不要过度使用。如果只是简单的版本号,用
project()自带的变量就够了。只有需要复杂逻辑时才用configure_file。
个人习惯:我会在模板文件头部加一行注释 // 自动生成,请勿手动修改。这样其他开发者看到生成的文件就知道去改模板,而不是直接改生成的文件。这个小习惯帮我省了不少沟通成本。
26.7 完整示例:一个生产级配置
最后给一个我在实际项目中用过的完整配置。这个例子结合了版本管理和功能开关:
# CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(DataPipeline VERSION 3.2.1 LANGUAGES CXX)
# 功能选项
option(BUILD_TESTS "Build unit tests" ON)
option(ENABLE_REDIS "Enable Redis support" ON)
option(ENABLE_KAFKA "Enable Kafka support" OFF)
option(ENABLE_MONITORING "Enable performance monitoring" ON)
# 依赖检测
find_package(OpenSSL QUIET)
if(OpenSSL_FOUND)
set(HAS_OPENSSL ON)
endif()
find_package(ZLIB QUIET)
if(ZLIB_FOUND)
set(HAS_ZLIB ON)
endif()
# 构建信息
string(TIMESTAMP BUILD_TIMESTAMP "%Y-%m-%d %H:%M:%S UTC" UTC)
# 生成配置头文件
configure_file(
"${PROJECT_SOURCE_DIR}/config.h.in"
"${PROJECT_BINARY_DIR}/config.h"
)
# 生成版本头文件
configure_file(
"${PROJECT_SOURCE_DIR}/version.h.in"
"${PROJECT_BINARY_DIR}/version.h"
)
# 让所有源文件都能找到生成的头文件
target_include_directories(${PROJECT_NAME} PRIVATE ${PROJECT_BINARY_DIR})
对应的 config.h.in:
#ifndef CONFIG_H
#define CONFIG_H
// 自动生成,请勿手动修改
// 功能开关
#cmakedefine BUILD_TESTS
#cmakedefine ENABLE_REDIS
#cmakedefine ENABLE_KAFKA
#cmakedefine ENABLE_MONITORING
// 依赖检测
#cmakedefine HAS_OPENSSL
#cmakedefine HAS_ZLIB
// 构建信息
#define BUILD_TIMESTAMP "@BUILD_TIMESTAMP@"
#endif // CONFIG_H
嗯,到这里你应该对 configure_file 有了比较全面的了解。它不复杂,但用好了能让你的项目结构清晰很多。下次改版本号或者加功能开关时,记得用这个方式,别再去手动改头文件了。
公众号:蓝海资料掘金营,微信deep3321