21、CMake自定义命令与目标:add_custom_command、add_custom_target、构建后处理

说实话,CMake 的构建系统本身已经很强大了——编译、链接、安装,一条龙服务。但总有那么些时候,你需要干点「私活」:比如生成一个头文件、跑个脚本、或者编译完后自动把产物扔到某个目录里。这时候,add_custom_commandadd_custom_target 就派上用场了。

我个人习惯把这两个命令看作是 CMake 的「瑞士军刀」。它们能让你在构建流程中插入任意操作,而且完全跨平台。嗯,今天我们就来把这把刀磨快一点。

21.1 add_custom_command:在构建中插入自定义动作

这个命令有两种用法,我分别叫它「文件生成型」和「事件挂钩型」。别被名字吓到,其实很好理解。

21.1.1 文件生成型:让 CMake 帮你生成文件

你想想看,有时候你的项目需要一些自动生成的代码,比如从 protobuf 文件生成 .pb.cc 和 .pb.h。你当然可以手动跑一次,但每次改完 .proto 文件都手动跑一遍?太累了。

add_custom_commandOUTPUT 形式,就能让 CMake 在构建时自动帮你生成这些文件:

add_custom_command(
    OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/generated/config.h
    COMMAND ${CMAKE_COMMAND} -E copy
        ${CMAKE_CURRENT_SOURCE_DIR}/config.h.in
        ${CMAKE_CURRENT_BINARY_DIR}/generated/config.h
    DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/config.h.in
    COMMENT "Generating config.h..."
)

这里的关键是 OUTPUTDEPENDS。CMake 会检查输出文件是否存在,以及依赖是否比输出新。如果依赖更新了,就重新执行命令。说白了,这就是 CMake 版的「增量构建」。

我的经验: 我在项目中遇到过一种情况——生成脚本本身也会变。这时候记得把脚本也加到 DEPENDS 里,否则你改了脚本,CMake 不会重新生成文件。踩过这个坑之后,我每次都会多问自己一句:「这个命令依赖什么?」

21.1.2 事件挂钩型:在编译前后干点事

另一种用法是挂在某个目标上,比如在编译完某个库之后,自动把产物复制到某个目录:

add_custom_command(
    TARGET MyLibrary
    POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E copy
        $<TARGET_FILE:MyLibrary>
        ${CMAKE_SOURCE_DIR}/output/libs/
    COMMENT "Copying library to output directory..."
)

这里 POST_BUILD 就是事件类型。CMake 支持三种:

事件类型 触发时机 典型用途
PRE_BUILD 目标开始编译之前 生成源文件、检查环境
PRE_LINK 编译完成、链接之前 修改目标文件、生成链接脚本
POST_BUILD 链接完成之后 复制产物、打包、部署
注意: PRE_BUILD 在 Visual Studio 生成器上行为有点特殊——它只在目标被标记为「需要构建」时才会触发。如果你需要每次都执行,建议用 PRE_LINK 或者单独搞个自定义目标。我曾经因为这个差异排查了半天,最后发现是生成器的问题。

21.2 add_custom_target:创建你自己的构建目标

add_custom_targetadd_custom_command 的区别,说白了就是一个是「动作」,一个是「目标」。自定义目标本身不产生任何输出文件,它就是一个名字,你可以用它来组织一系列操作。

最常见的用法是创建一个「一键打包」目标:

add_custom_target(Package
    COMMAND ${CMAKE_COMMAND} --build . --target MyApp
    COMMAND ${CMAKE_COMMAND} -E tar cfz myapp.tar.gz
        $<TARGET_FILE_DIR:MyApp>/
    COMMENT "Building and packaging MyApp..."
)

然后你只需要 cmake --build . --target Package,就能自动编译并打包。是不是很方便?

还有一个很实用的技巧——让自定义目标依赖其他目标:

add_custom_target(Deploy
    DEPENDS MyApp
    COMMAND ${CMAKE_COMMAND} -E copy
        $<TARGET_FILE:MyApp>
        /deploy/path/
)

这样,当你构建 Deploy 时,CMake 会先确保 MyApp 是最新的,然后再执行复制操作。嗯,这才是正确的依赖管理方式。

21.3 构建后处理:让构建结果更「干净」

构建后处理,说白了就是「编译完了,然后呢?」。我个人习惯在 POST_BUILD 里做三件事:

  1. 清理临时文件——比如编译过程中生成的中间文件,如果不需要保留,就删掉。
  2. 复制产物到统一目录——方便后续集成或测试。
  3. 生成版本信息文件——把 Git commit、构建时间等信息写到一个文件里。

举个例子,生成版本信息:

add_custom_command(
    TARGET MyApp
    POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E echo
        "BUILD_TIME=${CMAKE_CURRENT_TIME}"
        > ${CMAKE_CURRENT_BINARY_DIR}/version.txt
    COMMAND git rev-parse HEAD
        >> ${CMAKE_CURRENT_BINARY_DIR}/version.txt
    WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
)
核心思路: 构建后处理不是「锦上添花」,而是「自动化流水线」的一部分。你想想看,如果每次构建完都要手动复制、手动打包,那 CI/CD 还有什么意义?

21.4 知识体系总览

下面这张图是我自己总结的,把自定义命令和目标的关系梳理清楚了:

CMake 自定义命令与目标体系 add_custom_command 文件生成型 (OUTPUT) • 生成源文件、头文件 • 依赖检查:DEPENDS • 增量构建,避免重复执行 事件挂钩型 (TARGET) • PRE_BUILD / PRE_LINK / POST_BUILD • 复制、打包、生成版本信息 • 依赖目标自动触发 add_custom_target 自定义目标 • 不产生输出文件 • 通过 DEPENDS 组织依赖 • 可执行任意命令序列 典型用途 • 一键打包、部署 • 运行测试、代码检查 • 文档生成、代码格式化 可组合使用 核心原则:用 DEPENDS 管理依赖,用 COMMAND 定义动作

21.5 避坑指南与最佳实践

最后,分享几个我踩过的坑,希望能帮你省点时间:

  • 路径问题: 自定义命令的工作目录默认是构建目录。如果你需要切换到源码目录,记得用 WORKING_DIRECTORY 指定。我曾经忘记加这个,结果脚本找不到文件,排查了半天。
  • 跨平台命令: 尽量用 ${CMAKE_COMMAND} -E 提供的跨平台命令(如 copy、echo、tar),而不是直接写 cpcopy。这样你的 CMakeLists.txt 在 Windows 和 Linux 上都能跑。
  • 不要滥用: 自定义命令虽然强大,但能交给 CMake 原生功能做的事,就别自己写脚本。比如文件复制,用 configure_filefile(COPY ...) 更简洁。
  • 调试技巧: 如果自定义命令不执行,先检查 DEPENDS 是否正确。CMake 的增量构建逻辑很严格,依赖没变就不会执行。你可以临时加个 COMMAND echo "running..." 来验证。
我的习惯: 我会把所有的自定义命令和自定义目标集中放在一个 cmake/custom_targets.cmake 文件里,然后用 include() 引入。这样主 CMakeLists.txt 保持干净,维护起来也方便。

好了,关于自定义命令和目标,今天就聊到这里。记住,它们是 CMake 给你的「后门」,用好了能让你的构建流程如虎添翼。下次遇到「编译完还想干点啥」的需求,别犹豫,直接上 add_custom_commandadd_custom_target


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