12. 自定义命令与目标:add_custom_command(),add_custom_target(),生成源代码或文件
说实话,CMake 里最容易被低估的两个命令,就是 add_custom_command() 和 add_custom_target()。很多人觉得它们只是「跑个脚本」或者「复制个文件」,用不着深究。但我个人经验告诉我——恰恰是这两个命令,决定了你的构建系统是「能用」还是「好用」。
我记得刚接手一个大型嵌入式项目时,整个团队都在手动运行 Python 脚本生成配置头文件。每次改完配置,总有人忘记重新生成,导致编译出来的固件版本不对。后来我用 add_custom_command() 把生成步骤嵌进了构建流程,从此再没人抱怨过「头文件没更新」。
说白了,这两个命令就是 CMake 留给你的「后门」——当内置命令不够用时,你自己来定义构建规则。
12.1 核心概念:命令 vs 目标
先搞清楚一个基本区别:
- add_custom_command():定义一条「命令」,它依附于某个目标(target)或文件。只有当依赖发生变化时,它才会执行。
- add_custom_target():定义一个「目标」,它没有源文件,纯粹用来执行一组命令。它总是会被执行(除非你设置了依赖条件)。
你想想看,这就像 Git 里的 hook 和 workflow 的区别——一个是触发式的,一个是主动式的。
一句话总结:
- 想「生成文件」→ 用 add_custom_command()
- 想「执行动作」→ 用 add_custom_target()
12.2 add_custom_command():生成源代码或文件
这是最常用的场景。比如你有一个 protobuf 的 .proto 文件,需要在编译前生成 .pb.h 和 .pb.cc。或者你有一个代码生成器,需要根据 JSON 配置生成 C++ 源文件。
基本语法长这样:
add_custom_command(
OUTPUT generated_file.cpp
COMMAND python3 ${CMAKE_SOURCE_DIR}/tools/codegen.py
--input ${CMAKE_SOURCE_DIR}/config.json
--output ${CMAKE_CURRENT_BINARY_DIR}/generated_file.cpp
DEPENDS ${CMAKE_SOURCE_DIR}/config.json
${CMAKE_SOURCE_DIR}/tools/codegen.py
COMMENT "Generating source code from config.json..."
)
这里有几个关键点:
- OUTPUT:生成的文件路径。CMake 会根据这个文件是否存在、时间戳是否最新来判断是否需要重新执行命令。
- COMMAND:要执行的命令。可以写多条,按顺序执行。
- DEPENDS:依赖项。只要这些文件有变化,命令就会重新执行。
- COMMENT:构建时打印的提示信息,方便调试。
我的习惯:把生成的文件放到 CMAKE_CURRENT_BINARY_DIR 下,而不是源目录。这样 make clean 时能自动清理,不会污染源码树。
生成完文件后,怎么把它编译进目标呢?很简单,直接加到源文件列表里:
add_executable(my_app
main.cpp
${CMAKE_CURRENT_BINARY_DIR}/generated_file.cpp
)
CMake 会自动识别这个文件是由 add_custom_command() 生成的,并在编译前先执行生成命令。
12.3 add_custom_target():定义纯动作目标
有时候你不需要生成文件,只是想执行一些操作——比如打包、上传、运行测试。这时候 add_custom_target() 就派上用场了。
add_custom_target(package
COMMAND ${CMAKE_COMMAND} --build . --target install
COMMAND ${CMAKE_SOURCE_DIR}/scripts/package.sh
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
COMMENT "Creating distribution package..."
)
然后你就可以:
make package
注意,add_custom_target() 默认总是被执行。如果你想让它只在某些条件下执行,可以用 ALL 关键字控制是否加入默认构建,或者用 DEPENDS 指定依赖。
我曾经踩过的坑:有一次我写了一个自定义目标用来清理临时文件,结果忘了加 DEPENDS,每次构建都执行一遍清理,把生成的中间文件全删了……嗯,从那以后我养成了习惯:自定义目标要么加 DEPENDS,要么用 add_dependencies() 显式控制执行时机。
12.4 组合使用:命令 + 目标
最强大的用法,是把两者结合起来。比如你想让用户执行 make generate 来手动触发代码生成:
add_custom_command(
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/version.h
COMMAND python3 ${CMAKE_SOURCE_DIR}/tools/gen_version.py
--output ${CMAKE_CURRENT_BINARY_DIR}/version.h
DEPENDS ${CMAKE_SOURCE_DIR}/tools/gen_version.py
)
add_custom_target(generate
DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/version.h
)
这样,make generate 就会触发 add_custom_command() 生成 version.h。如果 version.h 已经是最新的,命令就不会重复执行。
12.5 实战:用代码生成器减少重复劳动
我在一个游戏引擎项目里用过这个模式。我们有几十个消息类型,每个类型都要写序列化/反序列化代码。手动写?太容易出错了。
于是我们写了一个 Python 脚本,读取 YAML 定义文件,自动生成 C++ 代码:
# 遍历所有消息定义文件
file(GLOB MESSAGE_DEFS ${CMAKE_SOURCE_DIR}/messages/*.yaml)
foreach(DEF ${MESSAGE_DEFS})
get_filename_component(NAME ${DEF} NAME_WE)
set(GENERATED_CPP ${CMAKE_CURRENT_BINARY_DIR}/messages/${NAME}.cpp)
set(GENERATED_HPP ${CMAKE_CURRENT_BINARY_DIR}/messages/${NAME}.hpp)
add_custom_command(
OUTPUT ${GENERATED_CPP} ${GENERATED_HPP}
COMMAND python3 ${CMAKE_SOURCE_DIR}/tools/message_gen.py
--input ${DEF}
--output-dir ${CMAKE_CURRENT_BINARY_DIR}/messages
DEPENDS ${DEF} ${CMAKE_SOURCE_DIR}/tools/message_gen.py
COMMENT "Generating message: ${NAME}"
)
list(APPEND GENERATED_SOURCES ${GENERATED_CPP})
endforeach()
add_library(messages ${GENERATED_SOURCES})
你看,几十个消息类型,一行代码都不用手写。改 YAML 文件,重新构建,代码自动更新。这就是自定义命令的威力。
12.6 常见陷阱与避坑指南
| 陷阱 | 说明 | 解决方案 |
|---|---|---|
| 命令不执行 | 明明改了依赖文件,但命令没重新跑 | 检查 DEPENDS 路径是否正确,尤其是相对路径 |
| 输出路径混乱 | 生成的文件放到了源目录,导致 git 提交混乱 | 统一使用 CMAKE_CURRENT_BINARY_DIR |
| 跨平台命令失败 | Windows 上 python3 不存在,或者路径分隔符问题 | 使用 find_program() 查找解释器,用 ${CMAKE_COMMAND} -E 代替系统命令 |
| 依赖循环 | 自定义目标依赖了自身,导致 CMake 报错 | 仔细检查 add_dependencies() 的依赖链 |
小技巧:调试自定义命令时,可以在 COMMAND 前加 echo 打印变量值。比如:COMMAND ${CMAKE_COMMAND} -E echo "Output dir: ${CMAKE_CURRENT_BINARY_DIR}"
12.7 知识体系图
下面这张图帮你理清本章的核心逻辑:
12.8 写在最后
自定义命令和自定义目标,是 CMake 构建系统里最灵活的部分。它们让你能跳出「编译 C++ 文件」这个框框,把任何自动化任务都纳入构建流程。
我个人建议:只要你的项目里有「手动执行脚本」的步骤,就想想能不能用 add_custom_command() 或 add_custom_target() 把它自动化。一开始可能觉得多写几行 CMake 麻烦,但长期来看,这省下的时间和避免的错误,绝对值回票价。
嗯,关于自定义命令和目标,就聊到这里。记住核心原则:生成文件用命令,执行动作用目标。两者配合,威力无穷。
公众号:蓝海资料掘金营,微信deep3321