外观
Stage07|Modern C++ Foundation
Lesson093|CMake 与一个可维护的 C++ 项目结构
Tags: #CMake #C++17 #BuildSystem #ProjectStructureDifficulty: ⭐⭐⭐⭐☆
一、问题场景:在自己机器能编译,CI 却找不到头文件
原因常不是 C++ 代码,而是工程把本机绝对路径、全局 include 和链接选项藏在 IDE 配置里。现代 CMake 的核心不是罗列命令,而是围绕 target 表达依赖和使用要求。
二、本课目标
- 区分 source tree 与 build tree。
- 使用 target 建模库和程序。
- 理解 PRIVATE/PUBLIC/INTERFACE。
- 配置测试、编译警告和多配置生成器。
- 形成可复现的构建命令。
三、回到 TaskSystem:把构建依赖表达出来
到这里项目至少包含 Task、队列、worker、测试和示例程序。CMake 的任务不是“让命令变短”,而是把公共头文件、实现库、线程库、测试和 Sanitizer 选项的关系写成 target。任何人从空 build 目录都能生成同样的依赖图,才算构建可维护。
四、建议项目结构
text
task-system/
├── CMakeLists.txt
├── include/task_system/task_queue.hpp
├── src/task_queue.cpp
├── app/main.cpp
└── tests/task_queue_tests.cpp构建产物放独立目录:
bash
cmake -S . -B build
cmake --build build --config Release不要把生成文件写回源码目录,也不要提交 build 目录。
五、Target 化 CMake
cmake
cmake_minimum_required(VERSION 3.20)
project(task_system LANGUAGES CXX)
add_library(task_system
src/task_queue.cpp
)
target_include_directories(task_system
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include
)
target_compile_features(task_system PUBLIC cxx_std_17)
add_executable(task_demo app/main.cpp)
target_link_libraries(task_demo PRIVATE task_system)task_demo 链接库后会继承库的 PUBLIC include 和 C++ 标准要求,不需要全局 include_directories()。
六、PRIVATE/PUBLIC/INTERFACE
text
PRIVATE:只构建当前 target 需要
PUBLIC:当前 target 需要,消费者也需要
INTERFACE:当前 target 自己不用,只传给消费者如果公共头文件暴露了某依赖类型,该依赖通常属于 PUBLIC;只在 .cpp 使用则通常 PRIVATE。
七、警告与平台差异
cmake
if(MSVC)
target_compile_options(task_system PRIVATE /W4 /permissive-)
else()
target_compile_options(task_system PRIVATE -Wall -Wextra -Wpedantic)
endif()不要把平台编译选项全局污染第三方库。Sanitizer、覆盖率和严格警告应通过选项或专用 target 作用到自有代码。
八、加入测试
cmake
include(CTest)
if(BUILD_TESTING)
add_executable(task_tests tests/task_queue_tests.cpp)
target_link_libraries(task_tests PRIVATE task_system)
add_test(NAME task_tests COMMAND task_tests)
endif()运行:
bash
cmake -S . -B build -DBUILD_TESTING=ON
cmake --build build --config Debug
ctest --test-dir build -C Debug --output-on-failure九、可复现构建
- 从空 build 目录配置 Debug 并运行测试。
- 再建立
build-release配置 Release。 - 故意删除 PUBLIC include,观察消费者失败。
- 恢复后生成
compile_commands.json(生成器支持时)。 - 在另一台机器或 CI 只执行文档中的三条命令。
实验通过条件是源码树不依赖本机绝对路径,清空 build 后仍能完整恢复。
十、常见故障
| 现象 | 优先检查 |
|---|---|
| 找不到头文件 | target_include_directories 可见性、路径、大小写 |
| unresolved symbol | 源文件是否属于 target、是否链接库、架构/配置 |
| Debug/Release 库混用 | 多配置 --config、运行库和 ABI |
| 修改 CMake 不生效 | 重新 configure,不只 build |
十一、Cocos 工程边界
Cocos Creator 2.4.x 的原生构建由编辑器和平台工程生成流程参与,课程中的独立 CMake 项目用于掌握共同基础。不要直接修补 Creator 的 build/、native/ 等生成目录;需要集成原生库时,应使用项目支持的扩展入口并记录目标平台、架构和 ABI。
十二、练习与答案
1. 为什么 target 比全局变量更可维护?
依赖、选项和 include 随具体库传播,作用域清楚,减少顺序依赖和对第三方 target 的污染。
2. CMake 是编译器吗?
不是。它配置并生成底层构建系统,再由编译器、链接器和 Ninja/MSBuild/Make 执行构建。
3. 为什么要 out-of-source build?
源码和产物隔离,可以并存多配置,也能安全清理和复现。
十三、本课总结
可维护构建应以 target 表达依赖,以独立 build tree 保存产物,以少量可复制命令完成配置、构建和测试。
