完整回顾 8 篇系列 + 全套改进方案 + 工业 CMake 开发者的成长路线
前言:这 8 篇我们学了什么?
从第一篇的「看不懂 CMakeLists.txt」,到第七篇的「测试、打包、安装」,我们走完了一个 CMake 开发者的完整成长路径。
回顾整个系列:
| 篇次 | 核心主题 | 你学会的能力 | 阶段 |
|---|---|---|---|
| 1 | CMake 实战开篇 | 看懂根 CMakeLists.txt,理解 CMake 工作流 | 🏗️ 看懂 |
| 2 | Target-Oriented 编程 | PUBLIC/PRIVATE/INTERFACE 选择 | 🎯 看懂 |
| 3 | 大型项目组织 | FZ2Helpers、CommonTargets、CommonOptions | 🏛️ 能改 |
| 4 | 条件编译与 GE | option() 层次、Generator Expressions 5 大场景 | 🔧 能改 |
| 5 | Qt 项目 CMake | AUTOMOC、翻译、windeployqt、ACSS | 🖥️ 专业化 |
| 6 | 第三方库集成 | find_package、FetchContent、函数封装 | 📦 集成 |
| 7 | 测试、打包、安装 | CTest、install()、CPack、包导出 | ✅ 交付 |
| 8 | 重构实战 | 综合运用,实战改进 | 🚀重构 |
一、Aether 的 10 个「好」—— 为什么这个项目值得学习
在 7 篇的分析中,我们看到了 Aether 项目 CMake 的很多优秀设计。这里做一个汇总:
1️⃣ 目标导向编程
所有 target 都使用target_*系列命令(target_include_directories、target_link_libraries、target_compile_definitions),没有使用旧式的全局命令。
# ✅ 好:使用 target_include_directories target_include_directories(common_extensionsystem PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/..> $<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}> )2️⃣ PUBLIC/PRIVATE/INTERFACE 设计清晰
每个 target 的 PUBLIC/PRIVATE 选择都有明确的设计意图。例如 extensionsystem 的PUBLIC Qt::Core+PRIVATE messagecenter,使用方不需要知道 messagecenter 的存在。
3️⃣ 命名空间别名系统
# CommonTargets.cmake fz2_add_common_library(extensionsystem SHARED ...) # 创建 common_extensionsystem + common::extensionsystem 别名 # 使用方通过别名链接 target_link_libraries(app PRIVATE common::extensionsystem)4️⃣ FZ2Helpers 函数库
10 个自定义函数消除了 60+ 个 CMakeLists.txt 中 90% 的重复代码,是 DRY 原则的典范。
5️⃣ 模块化选项系统
三层开关体系(硬件存在性 → 具体型号 → 示例程序),配合_fz2_gate_demo_if_off自动依赖传播。
6️⃣ 选项依赖自动传播
# Qt6 下自动关闭 ORM if(QT_VERSION_MAJOR EQUAL 6 AND COMMON_ORM_BUILD) set(COMMON_ORM_BUILD OFF CACHE BOOL "" FORCE) endif() # ORM 关闭时 Permission 也关闭 if(NOT COMMON_ORM_BUILD AND PLUGIN_PERMISSION_BUILD) set(PLUGIN_PERMISSION_BUILD OFF CACHE BOOL "" FORCE) endif()7️⃣ Generator Expression 大量使用
5 大场景覆盖了条件链接、条件参数、条件文件、条件目录、路径查询,是现代 CMake 的典范。
8️⃣ Qt6/Qt5 双版本支持
find_package(Qt6 COMPONENTS Core Gui Widgets QUIET) if(Qt6_FOUND) set(QT_VERSION_MAJOR 6) else() find_package(Qt5 5.15 REQUIRED COMPONENTS Core Gui Widgets) set(QT_VERSION_MAJOR 5) endif()9️⃣ 翻译自动化
.ts → .qm的全自动化流程,主程序和插件翻译都在构建后自动编译。
🔟 详细注释
每个add_compile_options的/wd都注释了原因,每段代码都有清晰的功能说明。
二、Aether 的 10 个「改进」方案—— 它可以更好
坦诚地说,Aether 的 CMake 虽然整体设计优秀,但也有一些可以改进的地方。发现并改进这些问题,正是从「优秀」到「卓越」的必经之路。
改进 1:file(GLOB_RECURSE) → 显式文件清单
现状:fz2_collect_sources使用file(GLOB_RECURSE)自动收集源文件。
问题:CMake 官方不推荐,新增文件时可能不会自动触发重新配置(虽然有CONFIGURE_DEPENDS)。
改进方案:对于核心模块,使用显式文件清单;对于文件数量多的模块,保持 GLOB_RECURSE 但有意识管理。
# 核心模块使用显式清单 set(CORE_SOURCES pluginmanager.cpp pluginloader.cpp pluginspec.cpp ) # 普通模块可以继续使用 GLOB_RECURSE(权衡后的选择) fz2_collect_sources(SOURCES HEADERS)改进 2:全局 MSVC 警告屏蔽 → per-target 控制
现状:根 CMakeLists.txt 使用add_compile_options()全局屏蔽 MSVC 警告。
问题:影响所有 target,包括第三方库。
改进方案:依赖fz2_target_msvc_options()函数,在每个 target 中单独设置:
# 每个 target 的 CMakeLists.txt 中 fz2_target_msvc_options(${PLUGIN_NAME})改进 3:硬编码 3rdparty 路径 → 封装变量 + find_package
现状:OpenCV、ZXing 等路径直接在 CMakeLists.txt 中硬编码。
问题:不可移植,升级版本需要改所有引用点。
改进方案:使用find_package或封装为变量:
# 在根 CMakeLists.txt 或 common/CMakeLists.txt 中定义 set(OPENCV_ROOT "${PROJECT_SOURCE_DIR}/common/3rdparty/opencv_4.10.0") find_package(OpenCV REQUIRED COMPONENTS core imgproc PATHS "${OPENCV_ROOT}" NO_DEFAULT_PATH ) # 在 camera/CMakeLists.txt 中使用 target_link_libraries(camera PRIVATE ${OpenCV_LIBS})改进 4:tests 被注释 → 用 option() 控制
现状:add_subdirectory(tests)被注释,没有文档说明原因。
改进方案:
# 根 CMakeLists.txt 中 option(BUILD_TESTING "Build unit/integration tests" OFF) if(BUILD_TESTING) enable_testing() include(CTest) add_subdirectory(tests) endif()改进 5:缺少 install() 命令
现状:没有任何install()命令,无法安装到系统。
改进方案:添加install(TARGETS ...)安装可执行文件、DLL、头文件。
改进 6:缺少 CPack 打包配置
现状:没有 CPack 配置,无法生成安装包。
改进方案:添加 CPack 配置,支持 NSIS 和 ZIP 格式。
改进 7:混用旧式 include_directories
现状:部分模块仍使用include_directories()(旧式全局命令)。
改进方案:全部改为target_include_directories()。
改进 8:手动设置输出目录 → 统一使用 fz2_setup_target
现状:部分 CMakeLists.txt 手动设置RUNTIME_OUTPUT_DIRECTORY_*。
改进方案:统一使用fz2_setup_target()函数。
改进 9:缺少 FetchContent 依赖管理
现状:所有第三方库手动拷贝到common/3rdparty/。
改进方案:对于 spdlog、googletest 等开源库,使用 FetchContent 管理。
改进 10:OpenCVConfig.cmake 存在但未使用
现状:OpenCV 的 Config 文件在common/3rdparty/opencv_4.10.0/lib/目录下,但项目没有使用它。
改进方案:使用find_package(OpenCV PATHS ...)而不是硬编码路径。
三、实战:重构 Camera 插件的 CMakeLists.txt
现在让我们把学到的知识应用到实战中——重构 Camera 插件的 CMakeLists.txt。
3.1 原版代码
# plugins/camera/CMakeLists.txt(原版) set(PLUGIN_NAME Camera) set(CISDK_MEASUREMENT_ROOT "${PROJECT_SOURCE_DIR}/common/3rdparty/cisdk/measurement") function(camera_link_cisdk_lib target lib_base_name) target_link_libraries(${target} PRIVATE "$<$<CONFIG:Debug>:${CISDK_MEASUREMENT_ROOT}/lib/Debug/${lib_base_name}d.lib>" "$<$<CONFIG:Release>:${CISDK_MEASUREMENT_ROOT}/lib/Release/${lib_base_name}.lib>" "$<$<CONFIG:RelWithDebInfo>:${CISDK_MEASUREMENT_ROOT}/lib/RelWithDebInfo/${lib_base_name}.lib>" ) endfunction() find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Gui Concurrent) fz2_collect_sources(PLUGIN_SOURCES PLUGIN_HEADERS) add_library(${PLUGIN_NAME} SHARED ${PLUGIN_SOURCES} ${PLUGIN_HEADERS} cameraplugin.json ) target_compile_features(${PLUGIN_NAME} PRIVATE cxx_std_17) target_compile_definitions(${PLUGIN_NAME} PRIVATE CAMERA_PLUGIN_LIBRARY) target_include_directories(${PLUGIN_NAME} PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include> PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/service ${PROJECT_SOURCE_DIR} ${PROJECT_SOURCE_DIR}/common ${PROJECT_SOURCE_DIR}/common/3rdparty ${PROJECT_SOURCE_DIR}/common/3rdparty/opencv_4.10.0/include ${PROJECT_SOURCE_DIR}/common/3rdparty/cisdk/measurement/include ${PROJECT_SOURCE_DIR}/common/core/config/include ${PROJECT_SOURCE_DIR}/common/utility/diagnostics ) target_link_libraries(${PLUGIN_NAME} PRIVATE Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Gui Qt${QT_VERSION_MAJOR}::Concurrent ${COMMON_TARGET_NAMESPACE}::extensionsystem ${COMMON_TARGET_NAMESPACE}::base ${COMMON_TARGET_NAMESPACE}::container ${COMMON_TARGET_NAMESPACE}::device_manager ${COMMON_TARGET_NAMESPACE}::image_capture ${COMMON_TARGET_NAMESPACE}::config ${COMMON_TARGET_NAMESPACE}::errors ${COMMON_TARGET_NAMESPACE}::logger $<$<CONFIG:Debug>:${PROJECT_SOURCE_DIR}/common/3rdparty/opencv_4.10.0/lib/opencv_world4100d.lib> $<$<NOT:$<CONFIG:Debug>>:${PROJECT_SOURCE_DIR}/common/3rdparty/opencv_4.10.0/lib/opencv_world4100.lib> ) foreach(_lib IN ITEMS algo_mat ci_utils) camera_link_cisdk_lib(${PLUGIN_NAME} ${_lib}) endforeach() fz2_target_msvc_options(${PLUGIN_NAME}) fz2_setup_plugin(${PLUGIN_NAME} "plugins/camera") fz2_target_source_group_tree(${PLUGIN_NAME} camera FILES ${PLUGIN_SOURCES} ${PLUGIN_HEADERS})3.2 问题分析
| 问题 | 位置 | 说明 |
|---|---|---|
| 硬编码路径 | 多个地方 | OpenCV、CISDK 路径直接写死 |
| 硬编码 OpenCV 版本 | opencv_4.10.0 | 版本号在路径中,升级要改所有引用 |
| 长路径重复 | 多处 | ${PROJECT_SOURCE_DIR}/common/3rdparty/...重复出现 |
| 三个配置写三次 | camera_link_cisdk_lib | 可以更简洁 |
3.3 重构后代码
# plugins/camera/CMakeLists.txt(重构版) set(PLUGIN_NAME Camera) # ── 第三方库路径集中管理 ────────────────────────────────────── set(_3RDPARTY "${PROJECT_SOURCE_DIR}/common/3rdparty") set(_OPENCV_ROOT "${_3RDPARTY}/opencv_4.10.0") set(_CISDK_MEASUREMENT_ROOT "${_3RDPARTY}/cisdk/measurement") # 使用 find_package 查找 OpenCV find_package(OpenCV REQUIRED COMPONENTS core imgproc PATHS "${_OPENCV_ROOT}" NO_DEFAULT_PATH ) # ── CISDK 链接函数(封装 Debug/Release/RelWithDebInfo)──────── function(camera_link_cisdk_lib target lib_base_name) target_link_libraries(${target} PRIVATE "$<$<CONFIG:Debug>:${_CISDK_MEASUREMENT_ROOT}/lib/Debug/${lib_base_name}d.lib>" "$<$<CONFIG:Release>:${_CISDK_MEASUREMENT_ROOT}/lib/Release/${lib_base_name}.lib>" "$<$<CONFIG:RelWithDebInfo>:${_CISDK_MEASUREMENT_ROOT}/lib/RelWithDebInfo/${lib_base_name}.lib>" ) endfunction() # ── Qt 依赖 ──────────────────────────────────────────────────── find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Gui Concurrent) # ── 源文件收集 ────────────────────────────────────────────────── fz2_collect_sources(PLUGIN_SOURCES PLUGIN_HEADERS) # ── 创建目标 ──────────────────────────────────────────────────── add_library(${PLUGIN_NAME} SHARED ${PLUGIN_SOURCES} ${PLUGIN_HEADERS} cameraplugin.json ) # ── 编译标准 ──────────────────────────────────────────────────── target_compile_features(${PLUGIN_NAME} PRIVATE cxx_std_17) target_compile_definitions(${PLUGIN_NAME} PRIVATE CAMERA_PLUGIN_LIBRARY) # ── 包含路径 ──────────────────────────────────────────────────── target_include_directories(${PLUGIN_NAME} PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include> PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/service ${PROJECT_SOURCE_DIR} ${PROJECT_SOURCE_DIR}/common ${_3RDPARTY} ${_OPENCV_ROOT}/include ${_CISDK_MEASUREMENT_ROOT}/include ${PROJECT_SOURCE_DIR}/common/core/config/include ${PROJECT_SOURCE_DIR}/common/utility/diagnostics ) # ── 链接依赖 ──────────────────────────────────────────────────── target_link_libraries(${PLUGIN_NAME} PRIVATE Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Gui Qt${QT_VERSION_MAJOR}::Concurrent ${COMMON_TARGET_NAMESPACE}::extensionsystem ${COMMON_TARGET_NAMESPACE}::base ${COMMON_TARGET_NAMESPACE}::container ${COMMON_TARGET_NAMESPACE}::device_manager ${COMMON_TARGET_NAMESPACE}::image_capture ${COMMON_TARGET_NAMESPACE}::config ${COMMON_TARGET_NAMESPACE}::errors ${COMMON_TARGET_NAMESPACE}::logger ${OpenCV_LIBS} # ⚡ 使用 find_package 的变量 ) foreach(_lib IN ITEMS algo_mat ci_utils) camera_link_cisdk_lib(${PLUGIN_NAME} ${_lib}) endforeach() # ── 编译选项 / 输出目录 / VS 分组 ──────────────────────────────── fz2_target_msvc_options(${PLUGIN_NAME}) fz2_setup_plugin(${PLUGIN_NAME} "plugins/camera") fz2_target_source_group_tree(${PLUGIN_NAME} camera FILES ${PLUGIN_SOURCES} ${PLUGIN_HEADERS})3.4 重构前后对比
| 方面 | 原版 | 重构版 | 改进 |
|---|---|---|---|
| 路径管理 | 硬编码 | 集中变量 | 一处修改,全局生效 |
| OpenCV | 硬编码 .lib | find_package+${OpenCV_LIBS} | 标准做法,自动路径 |
| 代码量 | 75 行 | 75 行 | 没有增加,但可维护性提升 |
| 可移植性 | 差 | 好 | 改路径只需改一个变量 |
| 版本升级 | 所有引用点改 | 改一个变量 | 维护成本降低 |
四、从零到一:你的 CMake 项目模板
基于 Aether 项目的经验,我为你设计了一个「最小但完整」的 CMake 项目模板。
4.1 项目结构
MyProject/ ├── CMakeLists.txt ← 根配置 ├── cmake/ ← 自定义 CMake 模块 │ ├── Helpers.cmake ← 辅助函数 │ ├── Targets.cmake ← 命名空间别名 │ └── Options.cmake ← 选项系统 ├── lib/ ← 公共库 │ ├── CMakeLists.txt │ ├── core/ │ │ └── CMakeLists.txt │ └── utility/ │ └── CMakeLists.txt ├── app/ ← 主程序 │ └── CMakeLists.txt ├── plugins/ ← 插件(可选) │ └── CMakeLists.txt └── tests/ ← 测试 └── CMakeLists.txt4.2 根 CMakeLists.txt
cmake_minimum_required(VERSION 3.16) project(MyProject VERSION 1.0.0 LANGUAGES CXX) # ── 构建类型 ───────────────────────────────────────────────── if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) set(CMAKE_BUILD_TYPE RelWithDebInfo CACHE STRING "Build type" FORCE) set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS Debug Release RelWithDebInfo MinSizeRel) endif() if(CMAKE_CONFIGURATION_TYPES) set(CMAKE_CONFIGURATION_TYPES "Debug;Release;RelWithDebInfo" CACHE STRING "Available build configurations" FORCE) endif() # ── C++ 标准 ───────────────────────────────────────────────── set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # ── 测试开关 ───────────────────────────────────────────────── option(BUILD_TESTING "Build unit/integration tests" OFF) # ── 编译数据库 ─────────────────────────────────────────────── set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # ── VS 文件夹 ──────────────────────────────────────────────── set_property(GLOBAL PROPERTY USE_FOLDERS ON) # ── 子目录 ─────────────────────────────────────────────────── add_subdirectory(lib) add_subdirectory(app) if(BUILD_TESTING) enable_testing() include(CTest) add_subdirectory(tests) endif()4.3 cmake/Helpers.cmake
# cmake/Helpers.cmake function(my_setup_target target vs_folder) set_target_properties(${target} PROPERTIES RUNTIME_OUTPUT_DIRECTORY_DEBUG "${CMAKE_BINARY_DIR}/bin/Debug" RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}/bin/Release" RUNTIME_OUTPUT_DIRECTORY_RELWITHDEBINFO "${CMAKE_BINARY_DIR}/bin/RelWithDebInfo" LIBRARY_OUTPUT_DIRECTORY_DEBUG "${CMAKE_BINARY_DIR}/bin/Debug" LIBRARY_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}/bin/Release" LIBRARY_OUTPUT_DIRECTORY_RELWITHDEBINFO "${CMAKE_BINARY_DIR}/bin/RelWithDebInfo" ARCHIVE_OUTPUT_DIRECTORY_DEBUG "${CMAKE_BINARY_DIR}/lib/Debug" ARCHIVE_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}/lib/Release" ARCHIVE_OUTPUT_DIRECTORY_RELWITHDEBINFO "${CMAKE_BINARY_DIR}/lib/RelWithDebInfo" FOLDER "${vs_folder}" ) endfunction() function(my_add_library basename type) add_library(my_${basename} ${type} ${ARGN}) if(NOT TARGET my::${basename}) add_library(my::${basename} ALIAS my_${basename}) endif() set(MY_LIBRARY_TARGET my_${basename} PARENT_SCOPE) set(MY_LIBRARY_ALIAS my::${basename} PARENT_SCOPE) endfunction()五、CMake 开发者进阶路线图
5.1 三个阶段
第一阶段:能看懂(第 1-2 篇) 能看懂根 CMakeLists.txt 能理解 target-oriented 编程 能修改现有项目的 CMake 第二阶段:能优化(第 3-5 篇) 能组织大型项目的 CMake 架构 能使用 Generator Expressions 能集成 Qt 和第三方库 第三阶段:能重写(第 6-8 篇) 能选择合适的第三方库集成方式 能配置测试、打包、安装 能从头设计一个项目的 CMake 架构5.2 推荐学习资源
书籍:
- Professional CMake: A Practical Guide(Craig Scott)—— 最权威的 CMake 书,必读
- Modern CMake for C++(Rafal Swidzinski)—— 现代 CMake 实践
官方文档:
- CMake 官方文档 —— 命令参考
- CMake 官方教程 —— 官方入门
开源项目学习:
- LLVM Project:大型项目的 CMake 架构典范
- Qt 6:Qt 官方 CMake 的最佳实践
- OpenCV:第三方库集成和模块化设计的标杆
- vcpkg:包管理器视角的 CMake 使用
5.3 持续学习建议
- 读源码:每当你用
find_package找到一个库,去看看它的XXXConfig.cmake文件是怎么写的 - 写项目:从零开始写一个 CMake 项目,把本系列 8 篇的知识点都用上
- 重构旧项目:把你现有的项目 CMake 重构一遍,你会遇到很多真实的问题
- 关注更新:CMake 每半年发布一个大版本,关注新特性
六、全系列总结
8 篇知识体系全景图
CMake 实战系列 8 篇 │ ├── 基础篇 │ ├── 第1篇:CMake 工作流、根 CMakeLists.txt 精读 │ └── 第2篇:Target-Oriented 编程、PUBLIC/PRIVATE/INTERFACE │ ├── 进阶篇 │ ├── 第3篇:大型项目组织、FZ2Helpers、CommonTargets │ ├── 第4篇:条件编译、option()、Generator Expressions │ └── 第5篇:Qt 项目 CMake 完整流程 │ └── 实战篇 ├── 第6篇:第三方库集成、find_package、FetchContent ├── 第7篇:测试、打包、安装、CTest、CPack └── 第8篇:重构实战、项目模板、进阶路线图核心能力
| 能力 | 对应命令 | 篇次 |
|---|---|---|
| 读懂 CMakeLists.txt | cmake_minimum_required、project()、add_subdirectory() | 1 |
| 目标导向编程 | target_*系列命令、PUBLIC/PRIVATE/INTERFACE | 2 |
| 模块化组织 | add_subdirectory、.cmake模块、函数 | 3 |
| 条件编译 | option()、$<CONFIG:Debug>、$<IF:$<...>,A,B> | 4 |
| Qt 自动化 | AUTOMOC、AUTORCC、windeployqt | 5 |
| 第三方库 | find_package、FetchContent、函数封装 | 6 |
| 测试交付 | enable_testing()、install()、CPack | 7 |
| 综合运用 | 重构实战、项目模板 | 8 |
最后的话
CMake 不是「语法」,而是「构建设计的语言」。
好的 CMake 架构 = 好的 C++ 项目架构。
当你写 CMakeLists.txt 时,你实际上是在设计项目的构建架构——模块如何划分、依赖如何组织、如何让其他开发者理解你的项目。
这篇系列结束了,但你的 CMake 之旅才刚刚开始。
去重构你的项目吧!
福利:互动与转发
💬 评论区留言:你的项目 CMake 最大的坑是什么?或者你从这篇系列中学到了什么?
📌 转发福利:转发本文到朋友圈,截图发到后台,可以领取:
- CMake 项目模板(基于本文第四节的设计)
- Aether 重构方案(本文第三节的完整重构代码)
🎯 下期预告:这个系列之后,我们将开启新的系列——《现代 C++ 项目架构实战》,从 CMake 扩展到项目架构设计、模块化、测试驱动开发等更广阔的领域。
感谢你 8 篇的陪伴,我们下个系列再见!
(注:本文档部分内容可能由 AI 辅助生成,但所有 Aether 代码分析均基于真实项目文件,经人工验证。)