第六章:目录与源文件管理
说到 CMake,很多人第一反应是「写 CMakeLists.txt 好麻烦」。其实不然。真正让人头疼的,是源文件一多,目录一深,整个构建系统就开始变得难以维护。我见过不少项目,CMakeLists.txt 里密密麻麻写满了 add_executable,每个源文件都要手动列一遍——说实话,这跟写 Makefile 有啥区别?
这一章,我们就来聊聊怎么优雅地管理源文件。核心就三个东西:aux_source_directory、file(GLOB ...),以及目录结构该怎么组织。
6.1 自动收集源文件:aux_source_directory
先看一个最基础的需求:你有一个 src/ 目录,里面放了十几个 .cpp 文件。难道要一个一个写进去?
add_executable(myapp
src/main.cpp
src/utils.cpp
src/network.cpp
src/database.cpp
# ... 还有十几个
)
嗯,我刚开始用 CMake 时就是这么干的。后来项目越来越大,每次新增文件都要改 CMakeLists.txt,烦得不行。直到我发现了 aux_source_directory。
6.1.1 基本用法
aux_source_directory(<dir> <variable>)
它会扫描指定目录下的所有源文件(.c、.cpp、.cxx 等),把文件名存到一个变量里。然后你直接用这个变量就行。
aux_source_directory(src SRC_LIST)
add_executable(myapp ${SRC_LIST})
就这么简单。以后你在 src/ 里加文件,CMake 会自动识别,不用再改 CMakeLists.txt。
6.1.2 实际项目中的坑
说实话,aux_source_directory 有个让我很头疼的问题:它是在 配置阶段 扫描目录的。也就是说,如果你新增了一个源文件,但没有重新运行 cmake 命令,构建系统是不知道这个新文件存在的。
我曾经踩过这个坑:在 IDE 里加了个新文件,直接点「构建」,结果链接失败,报了一堆未定义符号。折腾了半天才发现,原来是 CMake 没重新配置,新文件根本没被编译。
6.2 更灵活的方案:file(GLOB ...)
如果说 aux_source_directory 是「自动扫描目录」,那 file(GLOB ...) 就是「按模式匹配文件」。它更灵活,也更强。
6.2.1 基本语法
file(GLOB <variable> [LIST_DIRECTORIES true|false] [RELATIVE <path>] [<globbing-expressions>...])
说白了,就是支持通配符。比如:
file(GLOB SRC_LIST "src/*.cpp" "src/*.c")
add_executable(myapp ${SRC_LIST})
这会匹配 src/ 下所有 .cpp 和 .c 文件。比 aux_source_directory 更灵活的地方在于,你可以指定多个模式,甚至跨目录。
6.2.2 递归匹配
如果你有嵌套目录,可以用 GLOB_RECURSE:
file(GLOB_RECURSE SRC_LIST "src/**/*.cpp")
这会递归扫描 src/ 下所有子目录,找到所有 .cpp 文件。我个人习惯用这个,因为项目大了之后,目录层级往往很深。
file(GLOB_RECURSE SRC_LIST "src/**/*.cpp")
# 排除 build 目录
list(FILTER SRC_LIST EXCLUDE REGEX ".*/build/.*")
6.2.3 GLOB 的坑与最佳实践
嗯,这里要重点说一下。file(GLOB ...) 和 aux_source_directory 有同样的问题:不会自动检测新增文件。CMake 官方文档甚至明确建议「不要用 GLOB 来收集源文件」。
为什么?因为 CMake 的依赖追踪机制是基于文件的。你手动列出每个源文件时,CMake 知道「这个文件是构建输入的一部分」。但用 GLOB 时,CMake 只知道「这个目录被扫描过」,不知道具体有哪些文件。所以当你新增文件时,CMake 不会认为构建输入发生了变化,也就不会重新配置。
那怎么办?我的做法是:
- 小项目(少于 20 个源文件):手动列出,一劳永逸。
- 中等项目(20-100 个源文件):用 GLOB,但每次新增文件后手动重新配置。
- 大项目(100+ 源文件):用 GLOB + 自动化脚本,确保每次构建前自动重新配置。
6.3 目录结构组织建议
源文件管理,说到底不只是「怎么列文件」的问题。更重要的,是「文件该怎么放」。我见过太多项目,所有源文件堆在一个目录里,那叫一个乱。
6.3.1 推荐的分层结构
我个人习惯这样组织:
project_root/
├── CMakeLists.txt # 顶层 CMake
├── src/ # 源代码
│ ├── CMakeLists.txt # 子目录 CMake
│ ├── main.cpp
│ ├── module_a/
│ │ ├── CMakeLists.txt
│ │ ├── a.cpp
│ │ └── a.h
│ └── module_b/
│ ├── CMakeLists.txt
│ ├── b.cpp
│ └── b.h
├── include/ # 公共头文件
│ └── project/
│ ├── config.h
│ └── types.h
├── lib/ # 第三方库
├── tests/ # 测试代码
└── docs/ # 文档
每个模块有自己的 CMakeLists.txt,用 add_subdirectory 引入。这样每个目录的职责清晰,构建系统也容易维护。
6.3.2 模块化原则
你想想看,一个模块如果依赖了十几个其他模块,那这个模块本身就太「重」了。我建议遵循几个原则:
- 单一职责:每个模块只做一件事。
- 低耦合:模块之间通过接口(头文件)通信,不要直接依赖实现。
- 高内聚:相关的功能放在同一个目录下。
举个例子,网络模块就只负责网络通信,不要在里面混入数据库操作的代码。数据库模块同理。
6.3.3 头文件与源文件分离
我建议把公共头文件放在单独的 include/ 目录下,源文件放在 src/ 下。这样有几个好处:
- 安装时只需要分发
include/目录 - 源文件不会被外部依赖意外引用
- 目录结构更清晰
在 CMake 中,用 target_include_directories 指定头文件路径:
target_include_directories(myapp PUBLIC include/)
6.4 知识体系总览
说了这么多,我们来画一张图,把这一章的核心逻辑串起来:
6.5 总结
这一章我们聊了三个东西:
- aux_source_directory:简单粗暴,扫描目录收集源文件。适合小项目,但不递归。
- file(GLOB ...):更灵活,支持通配符和递归。但同样有「不自动检测新增文件」的问题。
- 目录结构组织:模块化、分层、头源分离。这是长期维护的关键。
说实话,没有银弹。每种方法都有适用场景,也都有坑。我的建议是:根据项目规模选择合适的方式,并且在团队内统一规范。别今天用 GLOB,明天又改手动列出,那才是真正的灾难。
嗯,下一章我们聊聊更高级的源文件管理技巧——比如怎么用 target_sources 来精细化控制源文件。不过那是后话了,先把这一章的内容消化掉再说。