29、CMake最佳实践:项目结构规范、变量命名、模块化设计、文档生成

说实话,CMake 这东西,很多人觉得能跑就行。但我在大厂带团队那几年,见过太多因为 CMake 写得烂导致的惨案——编译半天找不到头文件、变量名猜谜、模块耦合成一团浆糊。嗯,今天我就把压箱底的经验掏出来,跟你聊聊怎么写出「能传家」的 CMake 项目。

一、项目结构规范:别让文件夹变成垃圾堆

我接手过一个项目,源码和构建产物混在一起,第三方库散落在各个角落。你想想看,那是什么感觉?每次编译都要祈祷。后来我定了一套规矩,团队再也没为结构吵过架。

推荐的项目布局:

MyProject/
├── CMakeLists.txt          # 顶层构建文件
├── cmake/                  # 自定义模块和工具脚本
│   ├── FindMyLib.cmake
│   └── CompilerSettings.cmake
├── src/                    # 源代码
│   ├── CMakeLists.txt
│   ├── module_a/
│   └── module_b/
├── include/                # 公共头文件
│   └── MyProject/
├── tests/                  # 测试代码
│   ├── CMakeLists.txt
│   └── test_module_a.cpp
├── docs/                   # 文档
├── extern/                 # 第三方依赖(git submodule)
└── scripts/                # 辅助脚本

我个人习惯把 cmake/ 目录单独拎出来。为什么?因为 Find 模块和工具链配置是「基础设施」,混在源码里容易被人忽略。我曾经因为 Find 模块放在 src 下,结果重构时被误删,排查了一整天。

小技巧:顶层 CMakeLists.txt 只做三件事——设置项目属性、添加子目录、配置全局选项。别在里面写业务逻辑,那是子目录的事。

二、变量命名:别让后来人猜谜

变量命名这事,说白了就是「写给人看的」。我见过有人用 VAR1VAR2 这种名字,三个月后自己都看不懂。你想想看,这跟没写注释有什么区别?

我建议遵循以下规则:

类别 命名风格 示例
项目全局变量 大写下划线 MYPROJECT_VERSION
目录路径 大写下划线 + _DIR MYPROJECT_SOURCE_DIR
缓存变量 大写下划线 + CACHE MYPROJECT_ENABLE_TESTING
局部变量 小写下划线 target_sources
内部临时变量 下划线开头 _temp_list

注意:别用 set(var ...) 这种裸变量名。我曾经在大型项目里看到 set(LIBS ...),结果跟系统变量冲突,链接时一堆 undefined reference。血的教训。

还有一个坑:CMake 的变量作用域是「向下传递,向上隔离」。我在项目中遇到过子目录里改了父目录的变量,导致构建顺序错乱。解决方案很简单——用 set(... PARENT_SCOPE) 显式声明,或者干脆用 cache 变量。

三、模块化设计:别把鸡蛋放在一个篮子里

模块化设计,说白了就是「高内聚、低耦合」。CMake 里怎么做?用 add_librarytarget_link_libraries 把功能拆开。

我画了一张图,帮你理解模块间的依赖关系:

MyApp (可执行文件) CoreLib (静态库) NetworkModule (静态库) DataModule (静态库) Boost (外部依赖) SQLite (外部依赖) 应用层 核心层 模块层 外部依赖

你看,依赖是单向的。顶层应用依赖核心库,核心库依赖底层模块,底层模块依赖外部库。千万别搞出循环依赖——我在项目中遇到过 A 依赖 B,B 又依赖 A,结果 CMake 直接报错,重构了三天才理清。

模块化 CMake 示例:

# src/CMakeLists.txt
add_library(core_lib STATIC
    module_a/module_a.cpp
    module_b/module_b.cpp
)
target_include_directories(core_lib PUBLIC ${CMAKE_SOURCE_DIR}/include)
target_link_libraries(core_lib PUBLIC network_module data_module)

# 每个模块独立构建
add_library(network_module STATIC
    network/network.cpp
)
target_link_libraries(network_module PUBLIC Boost::boost)

add_library(data_module STATIC
    data/data.cpp
)
target_link_libraries(data_module PUBLIC SQLite::SQLite)

经验之谈:每个模块的 CMakeLists.txt 不要超过 50 行。如果超过,说明这个模块太「胖」了,该拆。我一般把公共编译选项抽到 cmake/CompilerSettings.cmake 里,用 include() 引入。

四、文档生成:别让后来人骂娘

文档这事,很多人觉得「代码即文档」。但 CMake 的配置逻辑复杂,光看代码真不一定能懂。我建议用 cmake --help 和 Doxygen 结合,自动生成文档。

具体做法:

  1. CMake 内嵌文档:# 注释写清楚每个选项的作用。比如:
# 启用测试,默认关闭。如果你要跑单元测试,把这个设为 ON
option(MYPROJECT_ENABLE_TESTING "Build with unit tests" OFF)
  1. 自动生成配置报告:在 CMakeLists.txt 末尾加一段:
# 打印构建配置摘要
message(STATUS "=== Build Configuration ===")
message(STATUS "Project Version: ${MYPROJECT_VERSION}")
message(STATUS "Build Type: ${CMAKE_BUILD_TYPE}")
message(STATUS "Testing Enabled: ${MYPROJECT_ENABLE_TESTING}")
message(STATUS "===========================")
  1. Doxygen 集成:docs/ 目录放一个 Doxyfile,然后在 CMake 里加一个自定义目标:
find_package(Doxygen)
if(DOXYGEN_FOUND)
    configure_file(${CMAKE_SOURCE_DIR}/docs/Doxyfile.in ${CMAKE_BINARY_DIR}/Doxyfile @ONLY)
    add_custom_target(docs
        COMMAND ${DOXYGEN_EXECUTABLE} ${CMAKE_BINARY_DIR}/Doxyfile
        WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
        COMMENT "Generating API documentation with Doxygen"
    )
endif()

注意:Doxygen 的配置里一定要设置 INPUTOUTPUT_DIRECTORY,否则它会扫描整个系统。我曾经没配好,结果它把 /usr/include 都扫进去了,生成了 2GB 的文档,编译服务器直接卡死。

嗯,到这里,CMake 最佳实践的核心内容就聊完了。项目结构、变量命名、模块化、文档生成,这四个点做好了,你的 CMake 项目就能「传家」了。记住,好的 CMake 配置,是团队协作的基石,也是你专业能力的体现。

一句话总结:写 CMake 就像写代码——结构清晰、命名规范、模块独立、文档齐全。别偷懒,偷懒的代价是后面十倍的时间来填坑。


公众号:蓝海资料掘金营,微信deep3321