CMake 实战终篇:Aether 项目 CMake 重构实战,从「能跑」到「专业」

📅 2026/8/7 9:22:32
CMake 实战终篇:Aether 项目 CMake 重构实战,从「能跑」到「专业」
完整回顾 8 篇系列 全套改进方案 工业 CMake 开发者的成长路线前言这 8 篇我们学了什么从第一篇的「看不懂 CMakeLists.txt」到第七篇的「测试、打包、安装」我们走完了一个 CMake 开发者的完整成长路径。回顾整个系列篇次核心主题你学会的能力阶段1CMake 实战开篇看懂根 CMakeLists.txt理解 CMake 工作流️ 看懂2Target-Oriented 编程PUBLIC/PRIVATE/INTERFACE 选择 看懂3大型项目组织FZ2Helpers、CommonTargets、CommonOptions️ 能改4条件编译与 GEoption() 层次、Generator Expressions 5 大场景 能改5Qt 项目 CMakeAUTOMOC、翻译、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::CorePRIVATE 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 虽然整体设计优秀但也有一些可以改进的地方。发现并改进这些问题正是从「优秀」到「卓越」的必经之路。改进 1file(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})改进 4tests 被注释 → 用 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 管理。改进 10OpenCVConfig.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硬编码 .libfind_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.txtcmake_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 GuideCraig Scott—— 最权威的 CMake 书必读Modern CMake for CRafal Swidzinski—— 现代 CMake 实践官方文档CMake 官方文档 —— 命令参考CMake 官方教程 —— 官方入门开源项目学习LLVM Project大型项目的 CMake 架构典范Qt 6Qt 官方 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.txtcmake_minimum_required、project()、add_subdirectory()1目标导向编程target_*系列命令、PUBLIC/PRIVATE/INTERFACE2模块化组织add_subdirectory、.cmake模块、函数3条件编译option()、$CONFIG:Debug、$IF:$...,A,B4Qt 自动化AUTOMOC、AUTORCC、windeployqt5第三方库find_package、FetchContent、函数封装6测试交付enable_testing()、install()、CPack7综合运用重构实战、项目模板8最后的话CMake 不是「语法」而是「构建设计的语言」。好的 CMake 架构 好的 C 项目架构。当你写 CMakeLists.txt 时你实际上是在设计项目的构建架构——模块如何划分、依赖如何组织、如何让其他开发者理解你的项目。这篇系列结束了但你的 CMake 之旅才刚刚开始。去重构你的项目吧福利互动与转发 评论区留言你的项目 CMake 最大的坑是什么或者你从这篇系列中学到了什么 转发福利转发本文到朋友圈截图发到后台可以领取CMake 项目模板基于本文第四节的设计Aether 重构方案本文第三节的完整重构代码 下期预告这个系列之后我们将开启新的系列——《现代 C 项目架构实战》从 CMake 扩展到项目架构设计、模块化、测试驱动开发等更广阔的领域。感谢你 8 篇的陪伴我们下个系列再见注本文档部分内容可能由 AI 辅助生成但所有 Aether 代码分析均基于真实项目文件经人工验证。