CMake目标依赖关系解析
直接回答:是的,当你执行 cmake --build build -t prebuilt
时,CMake 会去执行 ${prebuilt_target}
。
详细解释:为什么它会执行?
原因就在于你代码中的最后一行:add_dependencies(prebuilt ${prebuilt_target})
。
这行代码建立了一个明确的依赖关系。我们来分解一下:
-
prebuilt
是什么?
你提到“我并没有指定prebuilt这个变量作为一个target”,这通常是误解的关键。在你的CMakeLists.txt文件中,肯定在某处有一个地方通过add_custom_target
或add_executable
或add_library
创建了一个名为prebuilt
的目标。
例如,很可能有这样一行代码:add_custom_target(prebuilt ...)
这个
prebuilt
目标是一个“顶级”目标,它的唯一目的可能就是聚合其他像${prebuilt_target}
这样的子任务。 -
add_dependencies
的作用
add_dependencies(target-name depends-target1 depends-target2 ...)
这条命令的作用是告诉CMake:“在开始构建
target-name
之前,必须确保depends-target1
,depends-target2
等所有依赖项都已经成功构建。”这意味着,
prebuilt
目标依赖于${prebuilt_target}
目标。 -
构建时的逻辑链
当你运行cmake --build build -t prebuilt
(-t
是--target
的缩写)时,你是在命令CMake:“请构建名为prebuilt
的目标”。CMake的构建系统会这样处理:
- “用户要构建
prebuilt
。” - “检查一下,
prebuilt
依赖于${prebuilt_target}
。” - “好的,那我首先必须确保
${prebuilt_target}
是最新的并且已经执行。” - 执行
${prebuilt_target}
目标定义的命令(即你的cp -f
复制操作)。 - 一旦
${prebuilt_target}
执行完成,prebuilt
目标本身的构建(可能没有任何实际操作)就被认为完成了。
- “用户要构建
总结一下: 你不是通过“变量”来指定,而是通过 add_dependencies
命令,将 ${prebuilt_target}
添加为 prebuilt
这个已存在目标的依赖项。因此,构建 prebuilt
必然会先触发其所有依赖项的构建。
一个简单的类比
这就像是一个Makefile中的规则:
all: prebuiltprebuilt: copy_task@echo "All prebuilts are done."copy_task:cp file_a.txt /destination/
当你运行 make prebuilt
时,make
会首先去执行 copy_task
,因为它是一个依赖项。CMake中的 add_dependencies
就实现了类似 prebuilt: copy_task
的依赖关系。
额外建议和改进
你的代码虽然功能正确,但可以写得更现代、更清晰一些:
- 使用
cmake -E copy
代替bash -c
/cp
: 这使得命令与平台无关(在Windows上也能工作,无需bash)。 - 使用
DEPENDS
参数:add_custom_target
和add_custom_command
的DEPENDS
参数是更精确地指定文件级依赖关系的方式。 - 避免重复查找: 如果
${lib}
是一个明确的文件名,find
可能会在构建目录中意外找到多个同名文件。最好在配置阶段就确定库的完整路径。
改进后的版本可能类似于:
# 假设你已经在某个地方通过 find_library 或 get_target_property 获取了库的完整路径
# 并保存在一个变量里,比如 ${FULL_LIB_PATH}add_custom_target(${prebuilt_target} ALLCOMMAND${CMAKE_COMMAND} -E copy_if_different${FULL_LIB_PATH}${PREBUILT_LIB_PATH}/COMMENT "Copying ${lib} to prebuilt path: ${PREBUILT_LIB_PATH}"VERBATIM
)# 如果 prebuilt 目标存在,仍然添加依赖关系
if(TARGET prebuilt)add_dependencies(prebuilt ${prebuilt_target})
endif()
你可以直接使用 cmake --build <build_dir> -t <specific_target>
来只构建 prebuilt_target
集合中的某一个特定目标(比如一个通过 add_library
添加的库)。
这是一个非常常见且高效的开发工作流。
原理是什么?
这个功能的原理在于CMake构建系统的模块化和目标化设计:
-
每个目标都是独立的实体:
每当你在CMake中使用add_library(my_lib ...)
或add_executable(my_exe ...)
或add_custom_target(my_custom_target ...)
时,你都是在定义一个独立的、命名的构建目标。 -
生成的构建系统文件:
CMake会根据你的CMakeLists.txt
生成底层构建系统(如 Makefile 或 Ninja.build)的文件。在这个生成的系统中,每个CMake目标都会对应一个构建规则或目标。- 如果你使用
Makefile
生成器,会生成一个名为my_lib
的make
目标。 - 如果你使用
Ninja
生成器,会生成一个名为my_lib
的ninja
构建目标。
- 如果你使用
-
--target
(-t
) 参数的作用:
cmake --build
命令是一个跨平台的通用命令,它是对底层构建工具(如make
,ninja
,msbuild
等)的封装。--target
参数的作用就是将这个参数直接传递给底层的构建工具。所以,当你运行:
cmake --build build -t my_lib
它实际上等价于:
- 在Makefile生成器上:
cd build && make my_lib
- 在Ninja生成器上:
cd build && ninja my_lib
- 在Makefile生成器上:
-
依赖关系的自动处理:
底层构建工具(make/ninja)的核心功能就是处理依赖关系图。当你要求构建my_lib
时,构建工具会:- 查看它的依赖项(例如,由
target_link_libraries(my_lib PRIVATE some_dependency)
或add_dependencies(my_lib some_dependency)
定义的)。 - 自动地、按正确的顺序先去构建所有它依赖的、并且尚未构建或已过期的目标。
- 最后再构建
my_lib
本身。
- 查看它的依赖项(例如,由
举例说明
假设你的CMakeLists.txt如下:
cmake_minimum_required(VERSION 3.10)
project(MyProject)# 添加两个库目标
add_library(network_lib network.cpp)
add_library(math_lib math.cpp)# 添加一个可执行文件,依赖这两个库
add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE network_lib math_lib)# 添加一个自定义的“集合”目标,方便一键构建所有库
add_custom_target(prebuilt_libsDEPENDS network_lib math_lib # 这个custom target依赖于两个库目标COMMENT "All prebuilt libraries are up to date."
)
在这个项目中,你有多个可以独立构建的目标:
network_lib
math_lib
my_app
prebuilt_libs
你可以这样使用:
-
只构建
math_lib
及其依赖:cmake --build build -t math_lib
这会只编译
math.cpp
并生成libmath_lib.a
(或在Windows上是math_lib.lib
)。它不会碰network.cpp
或main.cpp
。 -
只构建
network_lib
:cmake --build build -t network_lib
-
构建整个应用程序
my_app
:cmake --build build -t my_app
这会自动先检查并构建
network_lib
和math_lib
(如果它们需要更新),然后再链接生成my_app
可执行文件。 -
构建你定义的“集合”目标
prebuilt_libs
:cmake --build build -t prebuilt_libs
这会触发构建
network_lib
和math_lib
,因为它们被列为DEPENDS
。
总结
- 可以:直接使用
-t <target_name>
构建任何一个在CMake中明确定义的目标(库、可执行文件、自定义目标),无论它是否是另一个更大目标的依赖项。 - 原理:CMake为每个目标在底层构建系统中生成了对应的规则。
--build -t
命令直接将目标名称传递给底层工具(make/ninja),由后者负责解析依赖图并执行最小范围的必要构建操作。
这是一种非常推荐的做法,特别是在大型项目中,可以极大地节省开发时的构建时间。你不需要每次都构建整个项目,只需构建你正在修改的那个模块即可。