6、cmake多目录执行流程
约 2829 字大约 9 分钟
2025-04-26
编写者:bugcode
本文已完成并校对,内容引用自 计算机自学指南
多目录 CMake 项目的执行流程
大型c++项目总,每一个子模块都有cmkelist配置文件,执行cmake后,每一个子模块中的cmkaelist都会被执行么?
- 是的,所有 CMakeLists.txt 都会被执行,但不是直接执行,而是通过根目录的 CMakeLists.txt 中的
add_subdirectory()命令递归地调用每个子目录的 CMakeLists.txt。
目录
1. 多目录 CMake 项目结构
1.1 典型项目结构
MyProject/ # 项目根目录
├── CMakeLists.txt # 根目录 CMakeLists (入口点)
├── build/ # 构建目录 (执行 cmake 的地方)
├── src/ # 源码目录
│ ├── CMakeLists.txt # 源码目录的 CMakeLists
│ ├── main.cpp
│ └── module1/ # 模块1
│ ├── CMakeLists.txt # 模块1的 CMakeLists
│ ├── module1.cpp
│ └── module1.h
├── lib/ # 库目录
│ ├── CMakeLists.txt # 库目录的 CMakeLists
│ ├── core/ # 核心库
│ │ ├── CMakeLists.txt
│ │ ├── core.cpp
│ │ └── core.h
│ └── utils/ # 工具库
│ ├── CMakeLists.txt
│ ├── utils.cpp
│ └── utils.h
├── tests/ # 测试目录
│ ├── CMakeLists.txt # 测试目录的 CMakeLists
│ ├── test_main.cpp
│ └── test_module1.cpp
└── third_party/ # 第三方库
├── CMakeLists.txt # 第三方库的 CMakeLists
└── fmt/ # fmt 库
└── CMakeLists.txt1.2 每个 CMakeLists.txt 的职责
| 目录 | 职责 | 典型内容 |
|---|---|---|
| 根目录 | 项目配置、全局设置、组织子目录 | 设置 C++ 标准、全局选项、添加子目录 |
| src/ | 主程序配置 | 添加可执行文件、链接库 |
| lib/ | 库的集合 | 添加子目录到各个库 |
| lib/core/ | 核心库 | 定义 core 库目标 |
| lib/utils/ | 工具库 | 定义 utils 库目标 |
| tests/ | 测试配置 | 添加测试可执行文件、链接测试框架 |
| third_party/ | 第三方库管理 | 使用 FetchContent 或 add_subdirectory |
2. CMake 执行流程概览
2.1 执行流程图
┌─────────────────────────────────────────────────────────┐
│ 用户在 build 目录执行 cmake .. │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 1. 读取根目录 CMakeLists.txt │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 2. 执行根目录的 cmake_minimum_required │
│ 和 project() 命令 │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 3. 设置全局变量和选项 │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 4. 遇到 add_subdirectory() 命令 │
└─────────────────────────────────────────────────────────┘
↓
┌────────────────┼────────────────┐
↓ ↓ ↓
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 执行子目录1的 │ │ 执行子目录2的 │ │ 执行子目录3的 │
│ CMakeLists.txt │ │ CMakeLists.txt │ │ CMakeLists.txt │
└─────────────────┘ └─────────────────┘ └─────────────────┘
↓ ↓ ↓
(可能继续添加子目录) (可能继续添加子目录) (可能继续添加子目录)
↓ ↓ ↓
┌────────┐ ┌────────┐ ┌────────┐
↓ ... ↓ ↓ ... ↓ ↓ ... ↓2.2 关键点
- 不是自动执行:CMake 不会自动寻找所有 CMakeLists.txt
- 通过 add_subdirectory 触发:每个子目录必须被父目录明确添加
- 深度优先执行:遇到 add_subdirectory 立即进入子目录执行
- 递归处理:子目录可以继续添加更多子目录
3. 详细执行步骤
3.1 步骤 1:根目录 CMakeLists.txt 开始执行
# 根目录 CMakeLists.txt
cmake_minimum_required(VERSION 3.15) # ← 首先执行
project(MyProject VERSION 1.0.0) # ← 然后执行
# 设置全局变量
set(CMAKE_CXX_STANDARD 17) # ← 执行
set(CMAKE_CXX_STANDARD_REQUIRED ON) # ← 执行
# 添加子目录 - 遇到这个命令会立即进入子目录
add_subdirectory(src) # ← 暂停根目录,进入 src
add_subdirectory(tests) # ← 从 src 返回后才执行
add_subdirectory(third_party) # ← 最后执行3.2 步骤 2:进入 src 子目录
# src/CMakeLists.txt
# 此时执行环境:已经继承了根目录的变量设置
# 添加这个子目录自己的设置
set(SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}) # ← 执行
# 继续添加更深层的子目录
add_subdirectory(module1) # ← 进入 module1
add_subdirectory(module2) # ← 从 module1 返回后执行
# 添加可执行文件
add_executable(myapp main.cpp) # ← 所有子目录返回后执行
target_link_libraries(myapp PRIVATE module1 module2)3.3 步骤 3:进入 module1 子目录
# src/module1/CMakeLists.txt
# 这是最深层的目录
# 定义这个模块的库
add_library(module1 STATIC module1.cpp) # ← 执行
# 设置这个库的包含目录
target_include_directories(module1 PUBLIC
${CMAKE_CURRENT_SOURCE_DIR} # ← 执行
)
# module1 执行完毕,返回上一层 (src)3.4 步骤 4:返回 src,继续执行
# 从 module1 返回 src/CMakeLists.txt
# 继续执行 add_subdirectory(module2)
add_subdirectory(module2) # ← 进入 module2
# module2 执行完毕后返回
# 继续执行后面的命令
add_executable(myapp main.cpp) # ← 现在执行
target_link_libraries(myapp PRIVATE module1 module2)
# src 执行完毕,返回根目录3.5 步骤 5:返回根目录,继续执行
# 从 src 返回根目录 CMakeLists.txt
# 继续执行下一个 add_subdirectory
add_subdirectory(tests) # ← 现在执行
add_subdirectory(third_party) # ← 然后执行
# 所有子目录处理完毕
message("配置完成") # ← 最后执行4. 变量作用域与传递
4.1 变量作用域规则
# 根目录 CMakeLists.txt
set(GLOBAL_VAR "我在任何地方都可见") # 普通变量,子目录可见
set(CACHE_VAR "缓存变量" CACHE STRING "描述") # 缓存变量,全局可见
# src/CMakeLists.txt
message("GLOBAL_VAR = ${GLOBAL_VAR}") # 可以访问
set(LOCAL_VAR "只在当前目录和子目录可见")
set(PARENT_VAR "父目录可见" PARENT_SCOPE) # 传递给父目录
# src/module1/CMakeLists.txt
message("LOCAL_VAR = ${LOCAL_VAR}") # 可以访问(继承自 src)4.2 变量传递示例
# 根目录
set(ROOT_VAR "root")
add_subdirectory(src)
message("FROM_SRC = ${FROM_SRC}") # 可以获取子目录传递的值
# src/CMakeLists.txt
message("ROOT_VAR = ${ROOT_VAR}") # 输出: root
set(SRC_VAR "src")
add_subdirectory(module1)
set(FROM_MODULE1 ${MODULE1_VAR} PARENT_SCOPE) # 传递给根目录
set(FROM_SRC "来自 src" PARENT_SCOPE) # 传递给根目录
# src/module1/CMakeLists.txt
message("SRC_VAR = ${SRC_VAR}") # 输出: src
set(MODULE1_VAR "module1" PARENT_SCOPE) # 传递给 src4.3 变量作用域示意图
┌─────────────────────────┐
│ 根目录 │
│ ROOT_VAR = "root" │
│ set(FROM_SRC) │
└──────────┬──────────────┘
│ add_subdirectory(src)
↓
┌─────────────────────────┐
│ src 目录 │
│ 看到: ROOT_VAR │
│ 设置: SRC_VAR │
│ 接收: MODULE1_VAR │
│ 传递: FROM_SRC │
└──────────┬──────────────┘
│ add_subdirectory(module1)
↓
┌─────────────────────────┐
│ module1 目录 │
│ 看到: ROOT_VAR, SRC_VAR│
│ 设置: MODULE1_VAR │
│ 传递: 给 src │
└─────────────────────────┘5. 执行顺序的控制
5.1 依赖关系管理
# 根目录 CMakeLists.txt
add_subdirectory(lib/core) # 先构建 core 库
add_subdirectory(lib/utils) # utils 可能依赖 core
add_subdirectory(src) # src 依赖所有库
add_subdirectory(tests) # tests 最后执行5.2 使用 target_link_libraries 建立依赖
# lib/utils/CMakeLists.txt
add_library(utils STATIC utils.cpp)
target_link_libraries(utils PRIVATE core) # utils 依赖 core
# 即使 core 在 utils 之后定义,CMake 也能处理
# 因为 target_link_libraries 只是声明依赖,不要求执行顺序5.3 条件执行
# 根目录 CMakeLists.txt
option(BUILD_TESTS "构建测试" ON)
add_subdirectory(src)
if(BUILD_TESTS)
add_subdirectory(tests) # 只有选项开启时才执行
endif()
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/third_party")
add_subdirectory(third_party) # 只有目录存在时才执行
endif()6. 实际案例演示
6.1 完整项目示例
根目录 CMakeLists.txt:
cmake_minimum_required(VERSION 3.15)
project(LargeProject VERSION 1.0.0)
message(STATUS "===== 开始配置项目 =====")
message(STATUS "源码目录: ${PROJECT_SOURCE_DIR}")
message(STATUS "构建目录: ${PROJECT_BINARY_DIR}")
# 全局设置
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# 全局变量
set(PROJECT_ROOT ${CMAKE_CURRENT_SOURCE_DIR})
set(COMMON_INCLUDES ${PROJECT_ROOT}/include)
message(STATUS "1. 配置核心库...")
add_subdirectory(lib)
message(STATUS "2. 配置应用程序...")
add_subdirectory(src)
option(BUILD_TESTS "构建测试" ON)
if(BUILD_TESTS)
message(STATUS "3. 配置测试...")
add_subdirectory(tests)
endif()
message(STATUS "===== 项目配置完成 =====")lib/CMakeLists.txt:
message(STATUS " - 进入 lib 目录")
# 按依赖顺序添加子目录
add_subdirectory(core) # 先构建 core,没有依赖
add_subdirectory(utils) # utils 依赖 core
add_subdirectory(network) # network 依赖 utils
message(STATUS " - lib 目录配置完成")lib/core/CMakeLists.txt:
message(STATUS " - 配置 core 库")
add_library(core STATIC
core.cpp
memory.cpp
threading.cpp
)
target_include_directories(core PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
${COMMON_INCLUDES}
)
target_compile_definitions(core PRIVATE
CORE_EXPORTS
$<$<CONFIG:Debug>:CORE_DEBUG>
)
message(STATUS " - core 库配置完成")lib/utils/CMakeLists.txt:
message(STATUS " - 配置 utils 库")
add_library(utils STATIC
string_utils.cpp
file_utils.cpp
time_utils.cpp
)
target_include_directories(utils PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
${COMMON_INCLUDES}
)
# 依赖 core 库
target_link_libraries(utils PRIVATE core)
message(STATUS " - utils 库配置完成")src/CMakeLists.txt:
message(STATUS " - 进入 src 目录")
# 收集源文件
set(APP_SOURCES
main.cpp
application.cpp
config.cpp
)
add_executable(myapp ${APP_SOURCES})
# 链接所有需要的库
target_link_libraries(myapp PRIVATE
core
utils
network
)
# 设置包含目录
target_include_directories(myapp PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${COMMON_INCLUDES}
)
message(STATUS " - 应用程序配置完成")6.2 执行时的输出
$ cd build
$ cmake ..实际输出:
-- ===== 开始配置项目 =====
-- 源码目录: /home/user/LargeProject
-- 构建目录: /home/user/LargeProject/build
-- 1. 配置核心库...
-- - 进入 lib 目录
-- - 配置 core 库
-- - core 库配置完成
-- - 配置 utils 库
-- - utils 库配置完成
-- - 配置 network 库
-- - network 库配置完成
-- - lib 目录配置完成
-- 2. 配置应用程序...
-- - 进入 src 目录
-- - 应用程序配置完成
-- 3. 配置测试...
-- - 进入 tests 目录
-- - 配置单元测试...
-- - 测试配置完成
-- ===== 项目配置完成 =====
-- Configuring done
-- Generating done7. 调试和验证方法
7.1 使用 message 跟踪执行流程
# 在任何 CMakeLists.txt 中添加
message(">>> 正在执行: ${CMAKE_CURRENT_SOURCE_DIR}/CMakeLists.txt")
# 或者使用更详细的信息
message(STATUS "[${CMAKE_CURRENT_LIST_LINE}] 在 ${CMAKE_CURRENT_LIST_FILE} 中执行")7.2 使用 --trace 选项
# 显示所有执行的 CMake 命令
cmake --trace ..
# 显示带变量展开的命令
cmake --trace-expand ..
# 只跟踪特定文件
cmake --trace-source=CMakeLists.txt ..
# 保存跟踪日志
cmake --trace --trace-redirect=trace.log ..跟踪输出示例:
/home/user/project/CMakeLists.txt(2): cmake_minimum_required(VERSION 3.15 )
/home/user/project/CMakeLists.txt(4): project(LargeProject VERSION 1.0.0 )
/home/user/project/CMakeLists.txt(10): set(CMAKE_CXX_STANDARD 17 )
/home/user/project/CMakeLists.txt(11): set(CMAKE_CXX_STANDARD_REQUIRED ON )
/home/user/project/CMakeLists.txt(18): add_subdirectory(lib )
/home/user/project/lib/CMakeLists.txt(1): add_subdirectory(core )
/home/user/project/lib/core/CMakeLists.txt(1): add_library(core STATIC core.cpp memory.cpp threading.cpp )
...7.3 查看生成的构建文件
# 查看所有定义的目标
grep -r "add_executable\|add_library" build/CMakeFiles/
# 查看目录处理顺序
grep -r "add_subdirectory" CMakeLists.txt */CMakeLists.txt
# 查看变量传递
grep -r "set(.*PARENT_SCOPE" CMakeLists.txt */CMakeLists.txt7.4 使用 CMake 的 --graphviz 选项
# 生成依赖图
cmake --graphviz=graph.dot ..
dot -Tpng graph.dot -o graph.png # 需要安装 graphviz
# 查看依赖关系
cat graph.dot8. 常见问题
8.1 问题:子目录 CMakeLists.txt 没被执行
症状:子目录中的配置没有生效
原因:父目录没有使用 add_subdirectory() 添加该子目录
解决方案:
# 检查父目录的 CMakeLists.txt
# 确保有这行
add_subdirectory(子目录名)
# 或者检查路径是否正确
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/subdir)8.2 问题:变量在子目录中不可见
症状:在根目录设置的变量,子目录中获取不到
原因:变量作用域问题
解决方案:
# 方法1:使用缓存变量
set(MY_VAR "value" CACHE INTERNAL "描述")
# 方法2:在根目录设置后立即使用
set(MY_VAR "value")
# 然后才添加子目录
add_subdirectory(src) # src 中可以看到 MY_VAR
# 方法3:使用 PARENT_SCOPE 向上传递
set(MY_VAR "value" PARENT_SCOPE)8.3 问题:目标重复定义
症状:错误信息 "add_library cannot create target ... because another target with the same name already exists"
原因:同一个目标被多次定义
解决方案:
# 检查是否重复 add_subdirectory
# 不要多次添加同一个子目录
# 或者使用条件判断
if(NOT TARGET mylib)
add_library(mylib STATIC ...)
endif()8.4 问题:执行顺序导致依赖错误
症状:链接时找不到库,但实际上库存在
原因:依赖的库在之后才定义,但 CMake 通常能处理,可能是其他问题
解决方案:
# CMake 不要求顺序,但可以显式指定依赖
target_link_libraries(myapp PRIVATE utils) # utils 可以在之后定义
# 如果一定要保证顺序,可以这样
add_subdirectory(lib/core) # 先构建依赖的
add_subdirectory(lib/utils) # 后构建依赖的
add_subdirectory(src) # 最后构建应用8.5 执行顺序总结表
| 执行阶段 | 执行内容 | 示例 |
|---|---|---|
| 深度优先 | 遇到 add_subdirectory 立即进入 | 先执行子目录,返回后继续 |
| 广度优先 | 按 add_subdirectory 顺序 | 先添加的先执行 |
| 条件执行 | 根据条件判断 | if() 内的才执行 |
| 依赖驱动 | 通过 target_link_libraries | 声明依赖,不强制顺序 |
总结
多目录 CMake 项目的执行流程:
- 从根目录 CMakeLists.txt 开始
- 遇到
add_subdirectory()就立即进入子目录执行 - 子目录执行完后返回父目录继续
- 所有目录按深度优先、从左到右的顺序执行
- 变量可以向下传递(默认),也可以向上传递(
PARENT_SCOPE) - 最终生成完整的构建系统
关键点:
- 不是自动执行所有 CMakeLists.txt,而是通过
add_subdirectory显式添加 - 执行顺序是深度优先的树遍历
- 每个子目录都有自己的变量作用域
- 可以使用
message()和--trace跟踪执行流程 - 依赖关系通过
target_link_libraries建立,不要求目录执行顺序