38、代码生成:使用configure_file()生成配置文件或头文件。
说实话,很多C++项目里都有这么个痛点:代码里硬编码了一堆路径、版本号、编译选项。换台机器编译,或者换个环境部署,就得手动改代码。我早年带团队时,就见过同事因为忘了改数据库连接串,把测试库的配置直接发到了生产环境……那场面,真是惨不忍睹。
其实CMake早就给了我们一个优雅的解决方案——configure_file()。说白了,它就是个模板引擎。你写一个带占位符的模板文件,CMake在配置阶段把变量填进去,生成真正的源码或配置文件。
核心用法:从模板到成品
先看个最简单的例子。假设你有一个头文件模板 config.h.in:
#ifndef CONFIG_H
#define CONFIG_H
#define PROJECT_VERSION "@PROJECT_VERSION@"
#define INSTALL_DIR "@CMAKE_INSTALL_PREFIX@"
#define DEBUG_MODE @DEBUG_MODE@
#endif
然后在CMakeLists.txt里这样写:
cmake_minimum_required(VERSION 3.10)
project(MyApp VERSION 1.2.3)
set(DEBUG_MODE 1)
configure_file(
config.h.in
${CMAKE_CURRENT_BINARY_DIR}/config.h
)
CMake配置完成后,build/config.h 里就会变成:
#ifndef CONFIG_H
#define CONFIG_H
#define PROJECT_VERSION "1.2.3"
#define INSTALL_DIR "/usr/local"
#define DEBUG_MODE 1
#endif
嗯,这里要注意:@VAR@ 这种写法是CMake的变量替换语法。你还可以用 ${VAR},但 @VAR@ 更常见,因为它不会跟CMake自身的变量展开混淆。
实战场景:版本信息自动注入
我在做嵌入式项目时,经常需要把Git提交哈希和编译时间写进固件里。这样出问题了,一看日志就知道是哪个版本。模板文件 version.h.in:
#pragma once
#include <string>
namespace app {
struct VersionInfo {
static constexpr int major = @PROJECT_VERSION_MAJOR@;
static constexpr int minor = @PROJECT_VERSION_MINOR@;
static constexpr int patch = @PROJECT_VERSION_PATCH@;
static constexpr const char* git_hash = "@GIT_HASH@";
static constexpr const char* build_time = "@BUILD_TIME@";
};
}
CMake里获取Git信息:
execute_process(
COMMAND git log -1 --format=%H
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
OUTPUT_VARIABLE GIT_HASH
OUTPUT_STRIP_TRAILING_WHITESPACE
)
string(TIMESTAMP BUILD_TIME "%Y-%m-%d %H:%M:%S")
configure_file(
version.h.in
${CMAKE_CURRENT_BINARY_DIR}/generated/version.h
)
这样每次CMake配置时,版本信息都是最新的。我个人习惯把生成的文件放到 generated/ 子目录下,方便跟手写的源码区分。
避坑指南:文件权限与路径
我曾经踩过一个坑:用 configure_file() 生成shell脚本,结果生成的脚本没有执行权限。后来才发现,configure_file() 默认不会保留源文件的权限属性。解决办法是手动设置:
configure_file(
deploy.sh.in
${CMAKE_CURRENT_BINARY_DIR}/deploy.sh
@ONLY
)
# 手动加执行权限
file(CHMOD ${CMAKE_CURRENT_BINARY_DIR}/deploy.sh
PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE
GROUP_READ GROUP_EXECUTE
WORLD_READ WORLD_EXECUTE)
另外,路径问题也容易翻车。如果你在模板里写了相对路径,生成后的文件路径是相对于 CMAKE_CURRENT_BINARY_DIR 的。我建议所有输出路径都用绝对路径,或者用 ${CMAKE_CURRENT_BINARY_DIR} 拼接。
高级技巧:条件生成与多平台适配
你想想看,一个项目要同时支持Windows和Linux,配置项肯定不一样。我们可以用CMake的变量来控制生成内容。模板文件 platform_config.h.in:
#ifndef PLATFORM_CONFIG_H
#define PLATFORM_CONFIG_H
// 平台检测
#if defined(_WIN32)
#define PLATFORM_NAME "Windows"
#define PATH_SEPARATOR '\\'
#elif defined(__linux__)
#define PLATFORM_NAME "Linux"
#define PATH_SEPARATOR '/'
#else
#error "Unsupported platform"
#endif
// 由CMake注入的配置
#define MAX_CONNECTIONS @MAX_CONNECTIONS@
#define LOG_LEVEL @LOG_LEVEL@
#define ENABLE_SSL @ENABLE_SSL@
#endif
CMake里根据平台设置不同值:
if(WIN32)
set(MAX_CONNECTIONS 100)
set(LOG_LEVEL "info")
set(ENABLE_SSL 0)
else()
set(MAX_CONNECTIONS 500)
set(LOG_LEVEL "debug")
set(ENABLE_SSL 1)
endif()
configure_file(
platform_config.h.in
${CMAKE_CURRENT_BINARY_DIR}/platform_config.h
)
这样做的好处是,平台相关的逻辑集中在CMake里,C++代码里只需要包含生成的头文件,不用写一堆 #ifdef 宏判断。
核心流程示意图
下面这张图展示了 configure_file() 的完整工作流程:
@ONLY 模式:控制替换范围
configure_file() 有个 @ONLY 参数,我建议你默认加上。为什么呢?因为如果不加,CMake会把模板里所有 ${VAR} 都尝试替换。万一你的模板文件里有 ${} 这种语法(比如某些脚本语言),就会被误替换,导致生成的文件语法错误。
用法很简单:
configure_file(
script.py.in
${CMAKE_CURRENT_BINARY_DIR}/script.py
@ONLY
)
这样CMake只会替换 @VAR@ 形式的变量,${VAR} 原样保留。
实战:生成JSON配置文件
除了头文件,configure_file() 也可以生成JSON、XML、INI等任何文本格式。比如生成一个数据库连接配置 db_config.json.in:
{
"host": "@DB_HOST@",
"port": @DB_PORT@,
"database": "@DB_NAME@",
"user": "@DB_USER@",
"password": "@DB_PASSWORD@",
"pool_size": @POOL_SIZE@,
"ssl_enabled": @SSL_ENABLED@
}
CMake里设置变量:
set(DB_HOST "192.168.1.100")
set(DB_PORT 5432)
set(DB_NAME "myapp_prod")
set(DB_USER "app_user")
set(DB_PASSWORD "secret123")
set(POOL_SIZE 10)
set(SSL_ENABLED true)
configure_file(
db_config.json.in
${CMAKE_CURRENT_BINARY_DIR}/config/db_config.json
@ONLY
)
生成后的JSON文件就是完整的配置,可以直接被程序读取。我个人习惯把敏感信息(比如密码)通过环境变量传入CMake,而不是硬编码在CMakeLists.txt里:
if(DEFINED ENV{DB_PASSWORD})
set(DB_PASSWORD $ENV{DB_PASSWORD})
else()
message(FATAL_ERROR "环境变量 DB_PASSWORD 未设置")
endif()
总结一下
configure_file() 的核心价值就四个字:分离配置。把可变的部分写成模板,把不变的部分留在CMake里。这样项目移植、环境切换都变得非常干净。
最佳实践清单:
- 模板文件后缀用
.in,一目了然 - 默认加
@ONLY,避免误替换 - 生成的文件放到
${CMAKE_CURRENT_BINARY_DIR}下,不要污染源码目录 - 敏感信息通过环境变量或CMake缓存变量传入
- 生成的文件要加入
.gitignore,不要提交到版本库
小技巧:如果你需要生成多个配置文件,可以写一个CMake函数封装 configure_file() 调用,避免重复代码。比如:
function(generate_config input output)
configure_file(
${input}
${CMAKE_CURRENT_BINARY_DIR}/${output}
@ONLY
)
endfunction()
generate_config(config.h.in config/config.h)
generate_config(db.json.in config/db.json)
注意:configure_file() 只在CMake配置阶段执行一次。如果你修改了模板文件或CMake变量,需要重新运行 cmake 命令(或点击IDE的重配置按钮)才能生效。增量编译不会触发重新生成。
好了,这一讲就到这里。记住:能用模板解决的问题,就别手写。这是我从无数次改配置改到崩溃中总结出来的教训。
公众号:蓝海资料掘金营,微信deep3321