21、配置与生成文件:configure_file、模板文件处理、版本头文件生成、配置时变量替换

说实话,CMake 里有个功能我特别喜欢,就是 configure_file。它看起来不起眼,但用好了能解决很多实际问题。我最早接触它是在做一个跨平台项目的时候,不同操作系统下头文件里的路径、版本号全都不一样,手动维护简直要疯掉。后来发现 CMake 早就帮我们想好了解决方案。

configure_file 的基本用法

configure_file 说白了就是一个「模板引擎」。它读取一个输入文件,把里面的 @变量名@ 或者 ${变量名} 替换成 CMake 变量的实际值,然后输出到另一个位置。

基本语法很简单:

configure_file(<input> <output> [COPYONLY] [ESCAPE_QUOTES] [@ONLY])
  • input:模板文件路径(通常是 .in 后缀)
  • output:生成的目标文件路径
  • COPYONLY:只复制,不替换变量
  • ESCAPE_QUOTES:自动转义引号
  • @ONLY:只替换 @var@ 形式,不替换 ${var}
我的习惯:模板文件统一用 .h.in.cpp.in 后缀,这样一眼就能看出哪些文件是模板。另外我一般只用 @var@ 语法,加上 @ONLY 选项,避免和 CMake 自身的 ${} 语法冲突。

实战:版本头文件生成

这是 configure_file 最经典的应用场景。你想想看,每次发布新版本都要手动改头文件里的版本号,多容易出错。我见过不止一个项目因为版本号写错导致线上事故。

先创建一个模板文件 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@
#define BUILD_TIMESTAMP "@BUILD_TIMESTAMP@"

#endif // VERSION_H

然后在 CMakeLists.txt 里配置:

cmake_minimum_required(VERSION 3.16)
project(MyApp VERSION 2.1.3)

# 获取构建时间戳
string(TIMESTAMP BUILD_TIMESTAMP "%Y-%m-%d %H:%M:%S")

# 生成版本头文件
configure_file(
    "${PROJECT_SOURCE_DIR}/cmake/version.h.in"
    "${PROJECT_BINARY_DIR}/generated/version.h"
    @ONLY
)

# 让编译器能找到生成的文件
target_include_directories(MyApp PRIVATE "${PROJECT_BINARY_DIR}/generated")

这样每次运行 CMake 配置时,版本号和时间戳都会自动更新。我在一个持续集成项目里用这个方案,每次 CI 构建都会生成带精确时间戳的版本信息,排查问题特别方便。

配置时变量替换的细节

这里有几个坑,我踩过之后记忆深刻:

我曾经踩过的坑:
  • 路径分隔符问题:Windows 下路径里的反斜杠会被当成转义字符。解决方案是用 file(TO_CMAKE_PATH) 先转换一下。
  • 布尔值替换:CMake 的 ON/OFF 在替换时不会自动转成 true/false。如果你需要 C++ 里的布尔值,得手动处理。
  • 引号嵌套:如果模板里有字符串包含引号,记得用 ESCAPE_QUOTES 选项。

举个例子,处理路径问题:

# 模板文件 config.h.in
#define DATA_DIR "@DATA_DIR@"

# CMakeLists.txt
# 先规范化路径
file(TO_CMAKE_PATH "${CMAKE_INSTALL_PREFIX}/share/MyApp" DATA_DIR)
configure_file(config.h.in config.h @ONLY)

SVG 流程图:configure_file 工作流程

configure_file 工作流程 模板文件 version.h.in CMake 变量 @PROJECT_VERSION@ 等 configure_file() 变量替换 + 输出 生成文件 version.h 版本头文件 版本号、时间戳 配置文件 路径、开关选项 构建信息 编译器、平台宏 许可证/版权 自动更新年份 输入模板 → 变量替换 → 输出目标文件 → 多种应用场景

COPYONLY 模式与条件配置

有时候你只是想把文件复制过去,不需要做任何替换。这时候用 COPYONLY 就行:

configure_file(
    "${PROJECT_SOURCE_DIR}/config/default.ini"
    "${PROJECT_BINARY_DIR}/config.ini"
    COPYONLY
)

嗯,这里要注意:COPYONLY 模式下如果源文件里有 @${},它们会被原样保留,不会报错。这个特性有时候可以用来做「条件配置」——比如根据不同的构建类型复制不同的配置文件。

我有个项目就是这么干的:

if(CMAKE_BUILD_TYPE STREQUAL "Debug")
    configure_file("config_debug.ini" "config.ini" COPYONLY)
else()
    configure_file("config_release.ini" "config.ini" COPYONLY)
endif()

高级技巧:嵌套模板与多文件生成

说实话,configure_file 不支持递归替换。也就是说,如果生成的文件里还有 @var@,它不会继续处理。但我们可以用「链式调用」来变相实现:

# 第一轮:替换项目级别的变量
configure_file("template_a.h.in" "intermediate.h" @ONLY)

# 第二轮:替换模块级别的变量
set(MODULE_SPECIFIC_VAR "custom_value")
configure_file("intermediate.h" "final_output.h" @ONLY)

这个技巧我在一个大型微服务项目里用过。每个服务共享一个基础模板,然后各自替换自己的专属配置。既减少了重复代码,又保持了灵活性。

避坑指南

几个容易忽略的点:

  • 输出目录必须存在configure_file 不会自动创建目录。如果输出路径的目录不存在,会报错。记得先用 file(MAKE_DIRECTORY) 创建。
  • 文件时间戳:只有内容发生变化时,输出文件的时间戳才会更新。这其实是个好特性——避免了不必要的重新编译。
  • 相对路径:输入和输出路径都建议用绝对路径,或者基于 PROJECT_SOURCE_DIR / PROJECT_BINARY_DIR 构建,避免工作目录不同导致的问题。
  • 编码问题:默认使用系统编码。如果你需要 UTF-8 输出,可以在模板文件里加 BOM 头,或者用 file(WRITE) 配合 file(READ) 手动处理。

我记得有一次帮同事排查问题,他死活说 configure_file 没生效。后来发现他把输出路径写成了源目录下的文件,结果每次 cmake --build 时生成的文件又被 git 还原了。嗯,输出到构建目录是个好习惯。

总结一下

configure_file 是 CMake 里一个轻量但强大的工具。它把「配置」和「构建」两个阶段优雅地连接起来。我个人觉得,只要你的项目需要根据不同的构建环境生成不同的代码文件,就应该考虑用它。

从版本头文件到配置文件,从许可证信息到构建时间戳,configure_file 的应用场景比你想象的要多。关键是掌握好变量替换的规则,避开那些常见的坑。

好了,这一章的内容就到这里。希望你在自己的项目里也能用好这个功能。


公众号:蓝海资料掘金营,微信 deep3321