多目录构建网络 - 将多个CMakeLists.txt组织成可控的构建系统
📚 课程目标
本课程将学习如何:
- 使用
add_subdirectory()组织多个子目录 - 管理大型项目的构建依赖关系
- 控制哪些模块被构建(条件构建)
- 在父子目录间传递变量和选项
- 理解CMake的作用域和变量可见性
- 构建一个可扩展的、模块化的CMake项目结构
📁 项目结构
06-多目录构建网络/ ├── CMakeLists.txt # 主CMakeLists.txt(根配置) ├── core/ # 核心库模块 │ ├── CMakeLists.txt │ ├── core_lib.h │ └── core_lib.cpp ├── utils/ # 工具库模块(可选) │ ├── CMakeLists.txt │ ├── utils_lib.h │ └── utils_lib.cpp ├── app/ # 主应用程序 │ ├── CMakeLists.txt │ └── main.cpp ├── examples/ # 示例程序(可选) │ ├── CMakeLists.txt │ ├── example1.cpp │ └── example2.cpp └── tests/ # 测试程序(可选) ├── CMakeLists.txt └── test_core.cpp🔍 核心概念详解
1. add_subdirectory() - 添加子目录
作用:将子目录添加到构建系统中,CMake会处理子目录中的CMakeLists.txt。
语法:
add_subdirectory(子目录路径)示例:
# 添加核心库子目录 add_subdirectory(core) # 条件添加工具库子目录 if(BUILD_UTILS) add_subdirectory(utils) endif()要点:
- 子目录路径相对于当前CMakeLists.txt所在目录
- 子目录中必须有CMakeLists.txt文件
- CMake会按照
add_subdirectory()的顺序处理子目录 - 子目录中创建的目标(target)在父目录中可见
2. 变量作用域和传递
2.1 变量作用域规则
重要规则:
- 父目录的变量对子目录可见:在父CMakeLists.txt中定义的变量,子目录可以直接使用
- 子目录的变量对父目录不可见:子目录中定义的变量不会影响父目录
- 使用
set(... PARENT_SCOPE)可以修改父目录的变量
示例:
# 父目录 CMakeLists.txt set(PROJECT_ROOT ${CMAKE_CURRENT_SOURCE_DIR}) set(BUILD_TESTS ON) add_subdirectory(core) # 子目录可以使用 PROJECT_ROOT 和 BUILD_TESTS # 子目录 core/CMakeLists.txt message(STATUS "项目根目录: ${PROJECT_ROOT}") # ✓ 可以使用 message(STATUS "构建测试: ${BUILD_TESTS}") # ✓ 可以使用 set(LOCAL_VAR "只在core目录中可见") # 父目录看不到这个变量2.2 使用 PARENT_SCOPE 修改父目录变量
# 子目录中 set(MY_VAR "新值" PARENT_SCOPE) # 修改父目录的变量3. 依赖关系管理
3.1 目标(Target)的可见性
关键点:
- 子目录中创建的目标(
add_library(),add_executable())在父目录和其他子目录中自动可见 - 可以在任何地方使用
target_link_libraries()链接这些目标
示例:
# core/CMakeLists.txt add_library(core_lib STATIC core_lib.cpp) # 创建目标 # utils/CMakeLists.txt add_library(utils_lib STATIC utils_lib.cpp) target_link_libraries(utils_lib PUBLIC core_lib) # ✓ 可以链接core_lib # app/CMakeLists.txt add_executable(main_app main.cpp) target_link_libraries(main_app core_lib utils_lib) # ✓ 可以链接两个库3.2 PUBLIC/PRIVATE/INTERFACE 的传递性
PUBLIC 的传递性:
# core/CMakeLists.txt target_include_directories(core_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) # PUBLIC 表示:core_lib需要这个目录,链接core_lib的目标也需要 # utils/CMakeLists.txt target_link_libraries(utils_lib PUBLIC core_lib) # 因为core_lib使用了PUBLIC包含目录,utils_lib会自动获得这个包含目录 # app/CMakeLists.txt target_link_libraries(main_app core_lib) # main_app也会自动获得core_lib的PUBLIC包含目录依赖链:
core_lib (PUBLIC包含目录) ↓ utils_lib (链接core_lib) ↓ main_app (链接core_lib和utils_lib)4. 条件构建
4.1 使用 option() 定义选项
# 主CMakeLists.txt option(BUILD_TESTS "构建测试程序" OFF) option(BUILD_EXAMPLES "构建示例程序" ON) option(BUILD_UTILS "构建工具库" ON)4.2 条件添加子目录
# 根据选项决定是否添加子目录 if(BUILD_TESTS) enable_testing() add_subdirectory(tests) endif() if(BUILD_EXAMPLES) add_subdirectory(examples) endif()4.3 条件链接库
# app/CMakeLists.txt target_link_libraries(main_app core_lib) # 如果工具库被构建,则链接它 if(BUILD_UTILS) target_link_libraries(main_app utils_lib) target_compile_definitions(main_app PRIVATE BUILD_UTILS_LIB) endif()5. 项目组织最佳实践
5.1 目录结构建议
项目根目录/ ├── CMakeLists.txt # 主配置 ├── include/ # 公共头文件(可选) ├── src/ # 源代码(可选,或直接在子目录) ├── libs/ # 第三方库(可选) │ ├── lib1/ │ └── lib2/ ├── apps/ # 应用程序 │ ├── app1/ │ └── app2/ ├── tests/ # 测试 └── docs/ # 文档5.2 每个模块的CMakeLists.txt结构
# 1. 收集源文件 set(MODULE_SOURCES file1.cpp file2.cpp ) # 2. 创建目标(库或可执行文件) add_library(module_name STATIC ${MODULE_SOURCES}) # 3. 设置包含目录 target_include_directories(module_name PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ) # 4. 链接依赖 target_link_libraries(module_name PUBLIC dependency1 dependency2 ) # 5. 安装规则(可选) install(TARGETS module_name ...)🛠️ 实践步骤
步骤1:查看项目结构
cd06-多目录构建网络 tree /F# Windows# 或tree# Linux/Mac步骤2:创建构建目录并配置(默认选项)
mkdirbuildcdbuild cmake..观察输出:
- 查看各个模块的配置消息
- 注意哪些子目录被添加了
- 查看依赖关系
步骤3:编译项目
cmake --build.观察:
- 各个模块的编译顺序
- 依赖关系如何影响编译顺序
步骤4:运行程序
# Windows.\Debug\main_app.exe# 或.\main_app.exe# Linux/Mac./main_app步骤5:尝试不同的构建选项
5.1 禁用工具库
cd..rm-rf build# 或 rmdir /s /q build (Windows)mkdirbuildcdbuild cmake -DBUILD_UTILS=OFF..cmake --build..\main_app.exe观察:
- utils目录不会被处理
- main_app仍然可以编译(因为使用了条件编译)
5.2 只构建核心库和应用程序
cmake -DBUILD_UTILS=OFF -DBUILD_EXAMPLES=OFF -DBUILD_TESTS=OFF..cmake --build.5.3 构建所有模块
cmake -DBUILD_UTILS=ON -DBUILD_EXAMPLES=ON -DBUILD_TESTS=ON..cmake --build.步骤6:查看构建的依赖关系图
# 生成依赖关系图(需要安装Graphviz)cmake --graphviz=graph.dot..dot -Tpng graph.dot -o graph.png📖 CMakeLists.txt 逐行解析
主CMakeLists.txt
# 第1-2行:版本要求 cmake_minimum_required(VERSION 3.10) # 第5行:项目定义 project(MultiDirectoryProject VERSION 1.0.0 LANGUAGES CXX) # 第8-9行:全局C++标准设置 set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 这些设置会传递给所有子目录 # 第12-15行:定义全局选项 option(BUILD_TESTS "构建测试程序" OFF) option(BUILD_EXAMPLES "构建示例程序" ON) option(BUILD_UTILS "构建工具库" ON) # 这些选项可以在命令行通过 -D 参数修改 # 第18行:定义全局变量 set(PROJECT_ROOT ${CMAKE_CURRENT_SOURCE_DIR}) # 子目录可以使用这个变量 # 第32行:无条件添加核心库 add_subdirectory(core) # core目录必须被构建 # 第35-38行:条件添加工具库 if(BUILD_UTILS) add_subdirectory(utils) endif() # 第41行:添加应用程序 add_subdirectory(app) # 第44-47行:条件添加示例程序 if(BUILD_EXAMPLES) add_subdirectory(examples) endif() # 第50-54行:条件添加测试 if(BUILD_TESTS) enable_testing() # 启用CTest add_subdirectory(tests) endif()core/CMakeLists.txt
# 第7-9行:收集源文件 set(CORE_SOURCES core_lib.cpp ) # 第12行:创建静态库 add_library(core_lib STATIC ${CORE_SOURCES}) # 这个目标在整个项目中可见 # 第15-18行:设置包含目录(PUBLIC) target_include_directories(core_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} # core目录 ${PROJECT_ROOT}/include # 使用父目录定义的变量 ) # PUBLIC表示:链接core_lib的目标也会自动获得这些包含目录 # 第20-24行:设置目标属性 set_target_properties(core_lib PROPERTIES VERSION ${PROJECT_VERSION} # 使用父目录定义的版本 SOVERSION 1 )utils/CMakeLists.txt
# 第12行:创建工具库 add_library(utils_lib STATIC ${UTILS_SOURCES}) # 第20行:链接核心库 target_link_libraries(utils_lib PUBLIC core_lib) # PUBLIC表示:链接utils_lib的目标也会自动链接core_lib # 并且会自动获得core_lib的PUBLIC包含目录app/CMakeLists.txt
# 第29行:创建可执行文件 add_executable(main_app ${APP_SOURCES}) # 第37-40行:条件链接工具库 if(BUILD_UTILS) target_link_libraries(main_app utils_lib) target_compile_definitions(main_app PRIVATE BUILD_UTILS_LIB) # 定义宏,让代码知道工具库可用 endif()🎯 关键知识点总结
1. add_subdirectory() 的行为
- 自动处理子目录的CMakeLists.txt
- 子目录中的目标自动可见
- 变量从父目录传递到子目录(单向)
- 可以条件添加子目录
2. 变量作用域
| 位置 | 父目录变量 | 子目录变量 |
|---|---|---|
| 父目录 | ✓ 可见 | ✗ 不可见 |
| 子目录 | ✓ 可见 | ✓ 可见 |
3. 目标可见性
- 所有子目录创建的目标在整个项目中可见
- 可以在任何地方使用
target_link_libraries()链接 - 依赖关系由CMake自动解析
4. PUBLIC/PRIVATE/INTERFACE 传递性
- PUBLIC:当前目标需要,依赖者也需要(会传递)
- PRIVATE:只有当前目标需要(不传递)
- INTERFACE:只有依赖者需要(会传递,但当前目标不需要)
5. 条件构建模式
# 1. 定义选项 option(BUILD_MODULE "描述" ON) # 2. 条件添加子目录 if(BUILD_MODULE) add_subdirectory(module) endif() # 3. 条件链接(在需要的地方) if(BUILD_MODULE) target_link_libraries(my_target module_lib) endif()💡 扩展练习
练习1:添加新模块
尝试添加一个新模块network/,包含:
network/CMakeLists.txtnetwork/network_lib.h和network/network_lib.cpp- 在主CMakeLists.txt中添加选项
BUILD_NETWORK - 让应用程序可以选择性地链接网络库
练习2:创建模块化结构
将项目重构为:
项目/ ├── CMakeLists.txt ├── libs/ │ ├── core/ │ ├── utils/ │ └── network/ ├── apps/ │ ├── app1/ │ └── app2/ └── tests/练习3:使用函数简化重复代码
创建一个函数来简化库的创建:
function(create_library lib_name) add_library(${lib_name} STATIC ${ARGN}) target_include_directories(${lib_name} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ) endfunction() # 使用 create_library(core_lib core_lib.cpp)练习4:实现依赖检查
添加依赖检查,确保必需的模块被构建:
if(NOT BUILD_UTILS AND BUILD_EXAMPLES) message(FATAL_ERROR "示例程序需要工具库,请启用BUILD_UTILS") endif()❓ 常见问题
Q1: 子目录中的变量如何在父目录中使用?
A: 使用set(... PARENT_SCOPE):
# 子目录中 set(MY_VAR "值" PARENT_SCOPE)Q2: 如何控制子目录的编译顺序?
A: CMake会根据依赖关系自动确定编译顺序。如果A依赖B,B会先编译。
Q3: 可以在子目录中修改父目录的选项吗?
A: 可以,但不推荐。最好在根CMakeLists.txt中统一管理选项。
Q4: 如何查看所有可用的目标?
A: 在CMakeLists.txt中添加:
get_property(_targets DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY BUILDSYSTEM_TARGETS) foreach(_target ${_targets}) message(STATUS "目标: ${_target}") endforeach()Q5: 如何跳过某个子目录的构建?
A: 使用条件判断:
if(NOT SKIP_MODULE) add_subdirectory(module) endif()Q6: 子目录中的CMakeLists.txt必须创建目标吗?
A: 不一定。子目录可以只设置变量、定义函数等,不一定要创建目标。
🚀 实际应用场景
场景1:大型开源项目
项目/ ├── CMakeLists.txt ├── src/ # 源代码 │ ├── core/ │ ├── gui/ │ └── network/ ├── tests/ # 测试 ├── examples/ # 示例 └── tools/ # 工具场景2:多平台项目
if(WIN32) add_subdirectory(platform/windows) elseif(UNIX) add_subdirectory(platform/linux) endif()场景3:插件系统
option(BUILD_PLUGIN_A "构建插件A" ON) option(BUILD_PLUGIN_B "构建插件B" OFF) if(BUILD_PLUGIN_A) add_subdirectory(plugins/plugin_a) endif()📚 下一步学习
掌握了多目录构建后,你可以继续学习:
- CMake函数和宏:创建可重用的构建逻辑
- FindPackage:查找和使用第三方库
- CPack:打包和分发项目
- CTest:集成测试框架
- ExternalProject:管理外部依赖
🎓 总结
通过本课程,你学会了:
✅ 使用add_subdirectory()组织多个CMakeLists.txt
✅ 理解变量作用域和传递机制
✅ 管理模块间的依赖关系
✅ 实现条件构建和可选模块
✅ 构建可扩展的、模块化的项目结构
现在你已经掌握了将几十个小CMakeLists.txt组织成可控构建网络的核心技能!🎉
祝你学习愉快!🚀