23. 自定义命令与目标:add_custom_command、add_custom_target、生成文件、构建后操作
说实话,CMake 里最让我觉得「这才是构建系统该有的样子」的功能,就是自定义命令和目标。为什么?因为真实项目里,你不可能永远只编译 C++ 代码。你可能需要生成代码、复制资源、运行脚本、打压缩包……这些事,CMake 都能帮你管起来。
我最早接触这个功能,是在一个嵌入式项目里。我们需要在编译前自动生成一些配置文件,编译后再把固件拷贝到指定目录。当时我还在用 shell 脚本手动搞,后来发现 CMake 的 add_custom_command 和 add_custom_target 直接把这些流程嵌进了构建系统。嗯,从那以后,我再也没写过单独的构建脚本。
23.1 add_custom_command:给构建流程加点「私活」
add_custom_command 有两种用法,我分别讲清楚。
23.1.1 生成文件型(OUTPUT)
这种用法,说白了就是「告诉 CMake:我要通过执行某个命令,生成一个文件」。CMake 会帮你做依赖追踪——如果输出文件不存在,或者输入文件变了,命令就会重新执行。
add_custom_command(
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/generated_config.h
COMMAND ${CMAKE_COMMAND} -E echo "// Auto-generated" > ${CMAKE_CURRENT_BINARY_DIR}/generated_config.h
COMMAND echo "#define VERSION \"1.0.0\"" >> ${CMAKE_CURRENT_BINARY_DIR}/generated_config.h
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/version.txt
COMMENT "Generating config header..."
)
这里有个细节:COMMAND 可以写多条,它们会按顺序执行。我习惯把生成脚本写成一个单独的 Python 或 shell 文件,然后在 COMMAND 里调用它,这样更清晰。
CMAKE_CURRENT_BINARY_DIR 里,别放到源码目录。否则 cmake --build . --clean-first 的时候,生成的文件不会被清理,容易出问题。
23.1.2 构建事件型(PRE_BUILD / PRE_LINK / POST_BUILD)
这种用法,是在某个目标构建的前后执行命令。我用的最多的是 POST_BUILD,比如编译完可执行文件后,自动把它拷贝到输出目录。
add_executable(my_app main.cpp)
add_custom_command(
TARGET my_app
POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy $<TARGET_FILE:my_app> ${CMAKE_SOURCE_DIR}/deploy/
COMMENT "Copying my_app to deploy directory..."
)
注意看,这里用了生成器表达式 $<TARGET_FILE:my_app>。它会在构建时展开成目标文件的完整路径。我个人强烈建议:只要涉及目标路径,就用生成器表达式,别手写路径。手写路径在跨平台或者多配置生成器(比如 Visual Studio)下很容易翻车。
POST_BUILD 执行 copy 命令,结果路径里有空格,命令直接炸了。后来我改用 ${CMAKE_COMMAND} -E copy,CMake 自己会处理好路径转义。记住:跨平台时,用 ${CMAKE_COMMAND} -E 代替系统原生命令。
23.2 add_custom_target:没有输出文件的目标
有时候,你只是想执行一组命令,并不关心它生成什么文件。比如运行测试、打包、清理临时文件。这时候就用 add_custom_target。
add_custom_target(
run_tests
COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
COMMENT "Running all tests..."
)
这个目标默认不会被构建,除非你显式指定:cmake --build . --target run_tests。或者,你可以用 add_dependencies 把它挂到其他目标上。
你想想看,为什么叫「自定义目标」?因为它本质上就是一个名字,CMake 不会检查它有没有输出文件,每次构建都会执行它的命令。这和 add_custom_command 的「增量构建」逻辑完全不同。
23.3 生成文件与依赖追踪
这是很多人容易搞混的地方。我画个图帮你理清关系。
看到没?add_custom_command 生成的文件,可以被 target_sources 引用,这样 CMake 就知道:编译 my_app 之前,得先生成那个头文件。这就是依赖追踪的完整链条。
23.4 构建后操作:自动化收尾工作
构建完成后,你可能想干点啥?比如把产物打包、上传到服务器、或者发个通知。我一般用 POST_BUILD 事件来做这些。
add_executable(my_app main.cpp)
add_custom_command(
TARGET my_app
POST_BUILD
COMMAND ${CMAKE_COMMAND} -E tar cf ${CMAKE_BINARY_DIR}/my_app.tar $<TARGET_FILE_DIR:my_app>/
COMMAND ${CMAKE_COMMAND} -E echo "Package created: my_app.tar"
COMMENT "Packaging my_app..."
)
这里用了 ${CMAKE_COMMAND} -E tar,CMake 自带的打包功能,跨平台没问题。如果你需要更复杂的打包逻辑,可以调用外部的脚本。
add_custom_command的OUTPUT形式用于生成文件,支持增量构建。add_custom_command的TARGET形式用于在目标构建前后执行操作。add_custom_target用于执行一组命令,没有输出文件,每次都会执行。- 用生成器表达式(如
$<TARGET_FILE>)代替硬编码路径。 - 跨平台命令用
${CMAKE_COMMAND} -E前缀。
23.5 实战:一个完整的代码生成流程
我拿一个真实项目里的例子给你看。我们需要从 YAML 文件生成 C++ 代码,然后编译进可执行文件。
# 第一步:生成代码
add_custom_command(
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/generated/config.cpp
${CMAKE_CURRENT_BINARY_DIR}/generated/config.h
COMMAND python3 ${CMAKE_CURRENT_SOURCE_DIR}/tools/codegen.py
--input ${CMAKE_CURRENT_SOURCE_DIR}/config.yaml
--output ${CMAKE_CURRENT_BINARY_DIR}/generated/
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/config.yaml
${CMAKE_CURRENT_SOURCE_DIR}/tools/codegen.py
COMMENT "Generating config code from YAML..."
)
# 第二步:把生成的文件加入目标
add_library(config STATIC
${CMAKE_CURRENT_BINARY_DIR}/generated/config.cpp
${CMAKE_CURRENT_BINARY_DIR}/generated/config.h
)
target_include_directories(config PUBLIC
${CMAKE_CURRENT_BINARY_DIR}/generated
)
# 第三步:构建后自动清理临时文件
add_custom_command(
TARGET config
POST_BUILD
COMMAND ${CMAKE_COMMAND} -E remove -f
${CMAKE_CURRENT_BINARY_DIR}/generated/config.tmp
COMMENT "Cleaning up temporary files..."
)
这个流程里,codegen.py 和 config.yaml 任何一个变了,CMake 都会重新生成代码。而且生成的文件在构建目录里,不会污染源码树。干净利落。
嗯,说到这,我想起一个细节:DEPENDS 里我加上了 codegen.py 本身。为什么?因为脚本改了,生成逻辑就变了,当然要重新生成。很多人只依赖输入数据文件,忘了依赖脚本本身,结果脚本改了构建还用的旧产物……我吃过这个亏。
add_custom_command 里调用了外部脚本,记得把脚本本身也加到 DEPENDS 里。否则脚本改了,CMake 不会重新执行命令。
好了,自定义命令和目标这块,说白了就是让 CMake 帮你管理「编译之外的那些事」。用好它们,你的构建系统会变得非常灵活,而且自动化程度极高。我个人觉得,这是 CMake 里最值得花时间掌握的功能之一。