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 重新配置时执行。如果你改了模板文件,记得重新运行 cmakecmake --build 时加 --reconfigure
  • 字符串引号: 模板里如果值是字符串,记得在模板里加引号,比如 "@PROJECT_VERSION@",否则生成出来会缺少引号,编译报错。

23.5 知识体系图

下面这张图帮你理清 configure_file() 的核心流程:

configure_file() 核心流程 模板文件 config.h.in CMake 变量 PROJECT_VERSION BUILD_TIMESTAMP configure_file() 替换占位符 @ONLY 模式 生成文件 ${BINARY_DIR}/config.h C++ 源码使用 #include "config.h" 模板 + 变量 → 替换 → 生成 → 使用

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 能帮你省掉多少手动同步的麻烦?

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