Skip to content

Stage07|Modern C++ Foundation ​

Lesson093|CMake 与一个可维护的 C++ 项目结构 ​

Tags: #CMake #C++17 #BuildSystem #ProjectStructureDifficulty: ⭐⭐⭐⭐☆


一、问题场景:在自己机器能编译,CI 却找不到头文件 ​

原因常不是 C++ 代码,而是工程把本机绝对路径、全局 include 和链接选项藏在 IDE 配置里。现代 CMake 的核心不是罗列命令,而是围绕 target 表达依赖和使用要求。

二、本课目标 ​

  1. 区分 source tree 与 build tree。
  2. 使用 target 建模库和程序。
  3. 理解 PRIVATE/PUBLIC/INTERFACE。
  4. 配置测试、编译警告和多配置生成器。
  5. 形成可复现的构建命令。

三、回到 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

九、可复现构建 ​

  1. 从空 build 目录配置 Debug 并运行测试。
  2. 再建立 build-release 配置 Release。
  3. 故意删除 PUBLIC include,观察消费者失败。
  4. 恢复后生成 compile_commands.json(生成器支持时)。
  5. 在另一台机器或 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 保存产物,以少量可复制命令完成配置、构建和测试。