21、CMake自定义命令与目标:add_custom_command、add_custom_target、构建后处理
说实话,CMake 的构建系统本身已经很强大了——编译、链接、安装,一条龙服务。但总有那么些时候,你需要干点「私活」:比如生成一个头文件、跑个脚本、或者编译完后自动把产物扔到某个目录里。这时候,add_custom_command 和 add_custom_target 就派上用场了。
我个人习惯把这两个命令看作是 CMake 的「瑞士军刀」。它们能让你在构建流程中插入任意操作,而且完全跨平台。嗯,今天我们就来把这把刀磨快一点。
21.1 add_custom_command:在构建中插入自定义动作
这个命令有两种用法,我分别叫它「文件生成型」和「事件挂钩型」。别被名字吓到,其实很好理解。
21.1.1 文件生成型:让 CMake 帮你生成文件
你想想看,有时候你的项目需要一些自动生成的代码,比如从 protobuf 文件生成 .pb.cc 和 .pb.h。你当然可以手动跑一次,但每次改完 .proto 文件都手动跑一遍?太累了。
用 add_custom_command 的 OUTPUT 形式,就能让 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..."
)
这里的关键是 OUTPUT 和 DEPENDS。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_target 和 add_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 里做三件事:
- 清理临时文件——比如编译过程中生成的中间文件,如果不需要保留,就删掉。
- 复制产物到统一目录——方便后续集成或测试。
- 生成版本信息文件——把 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}
)
21.4 知识体系总览
下面这张图是我自己总结的,把自定义命令和目标的关系梳理清楚了:
21.5 避坑指南与最佳实践
最后,分享几个我踩过的坑,希望能帮你省点时间:
- 路径问题: 自定义命令的工作目录默认是构建目录。如果你需要切换到源码目录,记得用
WORKING_DIRECTORY指定。我曾经忘记加这个,结果脚本找不到文件,排查了半天。 - 跨平台命令: 尽量用
${CMAKE_COMMAND} -E提供的跨平台命令(如 copy、echo、tar),而不是直接写cp或copy。这样你的 CMakeLists.txt 在 Windows 和 Linux 上都能跑。 - 不要滥用: 自定义命令虽然强大,但能交给 CMake 原生功能做的事,就别自己写脚本。比如文件复制,用
configure_file或file(COPY ...)更简洁。 - 调试技巧: 如果自定义命令不执行,先检查
DEPENDS是否正确。CMake 的增量构建逻辑很严格,依赖没变就不会执行。你可以临时加个COMMAND echo "running..."来验证。
cmake/custom_targets.cmake 文件里,然后用 include() 引入。这样主 CMakeLists.txt 保持干净,维护起来也方便。
好了,关于自定义命令和目标,今天就聊到这里。记住,它们是 CMake 给你的「后门」,用好了能让你的构建流程如虎添翼。下次遇到「编译完还想干点啥」的需求,别犹豫,直接上 add_custom_command 或 add_custom_target。
公众号:蓝海资料掘金营,微信deep3321