第六章:目录与源文件管理

说到 CMake,很多人第一反应是「写 CMakeLists.txt 好麻烦」。其实不然。真正让人头疼的,是源文件一多,目录一深,整个构建系统就开始变得难以维护。我见过不少项目,CMakeLists.txt 里密密麻麻写满了 add_executable,每个源文件都要手动列一遍——说实话,这跟写 Makefile 有啥区别?

这一章,我们就来聊聊怎么优雅地管理源文件。核心就三个东西:aux_source_directoryfile(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。

小提示:aux_source_directory 不会递归扫描子目录。它只看当前目录一层。如果你有嵌套目录,需要每个子目录单独调用一次。

6.1.2 实际项目中的坑

说实话,aux_source_directory 有个让我很头疼的问题:它是在 配置阶段 扫描目录的。也就是说,如果你新增了一个源文件,但没有重新运行 cmake 命令,构建系统是不知道这个新文件存在的。

我曾经踩过这个坑:在 IDE 里加了个新文件,直接点「构建」,结果链接失败,报了一堆未定义符号。折腾了半天才发现,原来是 CMake 没重新配置,新文件根本没被编译。

注意:aux_source_directory 不会自动跟踪新增文件。每次添加/删除源文件后,必须重新运行 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 文件。我个人习惯用这个,因为项目大了之后,目录层级往往很深。

重要:GLOB_RECURSE 会扫描所有子目录,包括 build 目录!如果你在源码目录里构建,一定要排除 build 目录,否则会把构建目录里的临时文件也加进来。
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 不会认为构建输入发生了变化,也就不会重新配置。

那怎么办?我的做法是:

  1. 小项目(少于 20 个源文件):手动列出,一劳永逸。
  2. 中等项目(20-100 个源文件):用 GLOB,但每次新增文件后手动重新配置。
  3. 大项目(100+ 源文件):用 GLOB + 自动化脚本,确保每次构建前自动重新配置。
我的经验:如果你用 GLOB,可以在 CMakeLists.txt 里加一行注释,提醒自己「新增文件后请重新运行 cmake」。或者,干脆写个脚本,每次构建前自动执行 cmake。

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 知识体系总览

说了这么多,我们来画一张图,把这一章的核心逻辑串起来:

目录与源文件管理 · 知识体系 aux_source_directory 扫描目录,收集源文件 file(GLOB ...) 模式匹配,灵活收集 目录结构组织 模块化、分层设计 共同问题:不会自动检测新增文件 需要手动重新配置 CMake 最佳实践建议 小项目手动列出 · 中项目用 GLOB + 手动重配 · 大项目用 GLOB + 自动化 目录结构:模块化、头源分离、单一职责

6.5 总结

这一章我们聊了三个东西:

  • aux_source_directory:简单粗暴,扫描目录收集源文件。适合小项目,但不递归。
  • file(GLOB ...):更灵活,支持通配符和递归。但同样有「不自动检测新增文件」的问题。
  • 目录结构组织:模块化、分层、头源分离。这是长期维护的关键。

说实话,没有银弹。每种方法都有适用场景,也都有坑。我的建议是:根据项目规模选择合适的方式,并且在团队内统一规范。别今天用 GLOB,明天又改手动列出,那才是真正的灾难。

嗯,下一章我们聊聊更高级的源文件管理技巧——比如怎么用 target_sources 来精细化控制源文件。不过那是后话了,先把这一章的内容消化掉再说。


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