关键看生成文件用途:编译前需的(如头文件)用OUTPUT+add_dependencies;构建后操作(如打包)用POST_BUILD(仅限add_custom_target)。BYPRODUCTS适用于无确定输出的场景,如linter或动态路径生成。

add_custom_command 的两种执行时机怎么选
关键看你要生成的文件是编译前需要的(比如头文件、协议缓冲区代码),还是构建后要做的(比如打包、压缩、校验)。add_custom_command 本质只有两类触发方式:OUTPUT 和 PRE_BUILD/POST_BUILD,但后者仅对 add_custom_target 有效,不能直接挂到库或可执行目标上。
常见错误是把生成 proto.h 的命令写成 COMMAND + POST_BUILD —— 这会导致编译器在第一次编译时找不到头文件而失败。正确做法是用 OUTPUT 声明产出,并让 CMake 知道这个输出是某个源文件的依赖:
add_custom_command(
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/generated.pb.h
COMMAND protoc --cpp_out=${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/msg.proto
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/msg.proto
COMMENT "Generating protobuf headers"
)
然后必须显式告诉目标它依赖这个输出:
add_executable(myapp main.cpp)
add_dependencies(myapp ${CMAKE_CURRENT_BINARY_DIR}/generated.pb.h)
target_sources(myapp PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/generated.pb.h)
为什么 add_custom_target 配合 BYPRODUCTS 更适合“伪生成”场景
当命令不产生真实文件(比如运行 linter、触发 CI 通知),或产出文件路径动态不可知(如 git describe 写入版本字符串到临时变量),就不能靠 OUTPUT 触发重执行。BYPRODUCTS 是更安全的替代:它声明“这个命令可能生成这些文件”,但不强制要求它们存在;CMake 会把它当作构建图中的一个节点,支持 add_dependencies,且不会因缺失 byproduct 报错。
立即学习“C++免费学习笔记(深入)”;
典型用法:
-
BYPRODUCTS必须配合OUTPUT或COMMAND使用,单独写无效 - 若命令实际没生成对应文件,CMake 不报错,但下次构建仍会执行(除非加
VERBATIM+ 手动缓存) - 不要把
BYPRODUCTS当作“忽略输出检查”的捷径——它解决的是“产出不确定”问题,不是“不想管输出”
add_custom_target(update_version
COMMAND ${CMAKE_COMMAND} -E echo "$$(${GIT_EXECUTABLE} describe --tags)" > ${CMAKE_CURRENT_BINARY_DIR}/VERSION
BYPRODUCTS ${CMAKE_CURRENT_BINARY_DIR}/VERSION
COMMENT "Updating VERSION file"
)
COMMAND 中的路径和变量为什么总出错
CMake 在 add_custom_command 里展开变量是“惰性的”:所有 ${...} 在配置阶段(cmake ..)就被替换成绝对路径,而 $<...></...> 是生成器表达式,延迟到构建阶段(make/ninja)才求值。混用就会崩溃。
常见陷阱:
- 在
COMMAND里写${CMAKE_SOURCE_DIR}/script.py→ 没问题,但若脚本本身要读相对路径参数,就失效 - 想传当前构建配置(Debug/Release)给命令?必须用
$<CONFIG>,写成${CMAKE_BUILD_TYPE}会变成空或错误值 - Windows 上用
cmd.exe /c调命令时,%CD%不可靠,应统一用$<TARGET_FILE_DIR:xxx>或$<CURRENT_SOURCE_DIR>
正确示例(跨平台获取当前源目录并传给 Python 脚本):
add_custom_command(
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/config.json
COMMAND ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/gen_config.py
$<CURRENT_SOURCE_DIR>
$<CONFIG>
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/gen_config.py
)
自定义目标生成器(generator expression)的边界在哪
生成器表达式($<...>)不是万能的。它只能用于 CMake 已知上下文:比如 target_compile_definitions、source_group、add_custom_command(COMMAND)。但它不能出现在 if()、set() 或 shell 命令内部(除非该命令本身由 CMake 解析,如 COMMAND ${CMAKE_COMMAND} -E ...)。
容易被忽略的限制:
-
$<TARGET_PROPERTY:mylib,INTERFACE_INCLUDE_DIRECTORIES>只在 target-aware 上下文有效,不能塞进add_custom_command(OUTPUT ...)的文件名里 -
$<JOIN:...>和$<SEMICOLON>很有用,但嵌套过深(>3 层)会让 Ninja 构建失败,建议拆成多个add_custom_command - 调试技巧:运行
cmake --build . --verbose查看最终展开的命令,比猜$<...>合理性更直接
真正难的是让生成逻辑可复现——比如用 $<TARGET_FILE:mylib> 作为输入依赖时,若 mylib 尚未定义,CMake 配置会静默失败。务必确保 add_custom_command 出现在相关 add_library 之后,且依赖关系用 add_dependencies 显式连接。


















