第29章:目录操作:file(GLOB ...)与file(GLOB_RECURSE ...)收集源文件
说实话,很多刚接触CMake的朋友都会问一个问题:「我项目里几十个源文件,难道要一个一个手动写进CMakeLists.txt吗?」
嗯,当然不用。CMake早就替你想好了——file(GLOB ...)和file(GLOB_RECURSE ...)就是干这个的。我个人习惯叫它们「文件收集器」,说白了就是帮你把某个目录下符合条件的文件一次性抓出来。
但这里有个坑:很多人用着用着就翻车了。为什么?因为GLOB的行为和你想象的可能不太一样。今天我就把这两个命令掰开揉碎讲清楚,顺便分享一些我在项目中踩过的坑。
29.1 基本用法:file(GLOB ...)
file(GLOB ...)的语法其实很简单:
file(GLOB 变量名 文件匹配模式1 [文件匹配模式2 ...])
它会将匹配到的所有文件路径存入指定的变量中。举个例子:
# 收集 src 目录下所有 .cpp 文件
file(GLOB MY_SOURCES "src/*.cpp")
# 收集 include 目录下所有 .h 文件
file(GLOB MY_HEADERS "include/*.h")
# 然后就可以直接用了
add_executable(my_app ${MY_SOURCES} ${MY_HEADERS})
你看,是不是省事多了?不用再一个一个写src/main.cpp、src/utils.cpp……
*(匹配任意字符)和 ?(匹配单个字符)。比如 "src/*.cpp" 会匹配 src 目录下所有 .cpp 文件,但不会递归到子目录。
29.2 递归收集:file(GLOB_RECURSE ...)
如果你的项目有嵌套目录结构,比如:
src/
├── main.cpp
├── ui/
│ ├── window.cpp
│ └── dialog.cpp
└── utils/
├── string_utils.cpp
└── file_utils.cpp
用file(GLOB ...)只能抓到main.cpp,子目录里的文件一个都拿不到。这时候就该GLOB_RECURSE上场了:
file(GLOB_RECURSE ALL_SOURCES "src/*.cpp")
它会递归遍历src/下的所有子目录,把main.cpp、window.cpp、dialog.cpp……全部抓出来。
LIST_DIRECTORIES false 或者配合 REGEX 过滤。
29.3 核心知识点:GLOB 的「一次性」特性
这是最容易被忽略的一点,也是我当年踩过的大坑。
GLOB 只在 CMake 配置阶段执行一次。什么意思呢?你运行cmake ..的时候,GLOB 会扫描目录,把匹配到的文件列表存下来。但如果你之后在源码目录里新增或删除了文件,CMake 并不会自动重新扫描。
我曾经就因为这个吃过亏:在项目里加了一个新.cpp文件,然后直接make,结果链接报错说找不到符号。折腾了半天才发现——CMake 根本不知道我加了新文件,因为它没有重新运行配置。
解决方案有两个:
- 手动重新运行 cmake(最直接,但容易忘)
- 使用 CONFIGURE_DEPENDS 选项(CMake 3.12+ 支持)
# 加上 CONFIGURE_DEPENDS,CMake 会在构建时检查文件变化
file(GLOB MY_SOURCES CONFIGURE_DEPENDS "src/*.cpp")
加了CONFIGURE_DEPENDS之后,每次构建前 CMake 都会检查目录内容是否有变化。不过要注意,这个功能依赖构建系统的支持(比如 Makefile 和 Ninja 都支持,但某些 IDE 可能不行)。
29.4 进阶用法:过滤与排除
有时候你不想收集所有文件。比如测试文件、临时文件、或者某些特定后缀的文件。这时候可以用REGEX模式:
# 只收集 .cpp 文件,排除 _test.cpp 结尾的文件
file(GLOB_RECURSE SOURCES
CONFIGURE_DEPENDS
"src/*.cpp"
"!src/*_test.cpp"
)
# 或者用 RELATIVE 让路径变成相对路径
file(GLOB_RECURSE HEADERS RELATIVE "include" "include/*.h")
这里!表示排除匹配。我个人习惯把排除模式放在最后,这样逻辑更清晰。
29.5 实战建议:什么时候用 GLOB,什么时候不用?
这个问题其实挺有争议的。CMake 官方文档甚至建议不要用 GLOB,而是手动列出源文件。为什么?
原因就是前面说的「一次性」问题。手动列出文件虽然麻烦,但CMake 能准确知道依赖关系,新增文件时 CMakeLists.txt 会变化,CMake 会自动重新配置。
但我的观点是:看项目规模。
| 项目规模 | 推荐做法 | 理由 |
|---|---|---|
| 小项目(< 20 个源文件) | 手动列出 | 清晰可控,避免 GLOB 的坑 |
| 中等项目(20-100 个源文件) | GLOB + CONFIGURE_DEPENDS | 省事,且 CONFIGURE_DEPENDS 能解决大部分问题 |
| 大型项目(> 100 个源文件) | 手动列出或使用生成器 | 大型项目通常有复杂的构建逻辑,GLOB 不够灵活 |
我个人在中等规模的项目里用得最多。比如一个图形界面工具,几十个源文件分布在几个子目录里,用GLOB_RECURSE加上CONFIGURE_DEPENDS,既省事又可靠。
29.6 避坑指南
最后总结几个我踩过的坑,你遇到了可以少走弯路:
- 坑一:忘记 CONFIGURE_DEPENDS —— 新增文件后构建失败,排查半天才发现是 GLOB 没更新。现在我的所有 GLOB 都默认加上这个选项。
- 坑二:GLOB_RECURSE 扫到了 build 目录 —— 如果你在源码目录里构建,GLOB_RECURSE 可能会把 build 目录下的临时文件也扫进来。解决方案:把源码和构建目录分开,或者用
REGEX排除 build。 - 坑三:路径分隔符问题 —— Windows 下路径是反斜杠
\,Linux 下是正斜杠/。GLOB 返回的路径会使用当前平台的分隔符,如果你在跨平台项目里做字符串处理,要注意统一转换。 - 坑四:GLOB 不支持变量展开 —— 比如
file(GLOB FILES "${MY_DIR}/*.cpp")是没问题的,但如果你写成file(GLOB FILES $ENV{MY_DIR}/*.cpp),环境变量可能不会按你预期的方式展开。
29.7 知识体系总览
下面这张图帮你理清 GLOB 的核心逻辑:
核心要点回顾:
file(GLOB ...)只扫描当前目录,file(GLOB_RECURSE ...)递归扫描所有子目录- GLOB 只在 CMake 配置阶段执行一次,新增文件后需要重新运行 cmake
- CMake 3.12+ 支持
CONFIGURE_DEPENDS选项,让构建时自动检查文件变化 - 用
REGEX和!可以灵活过滤和排除文件 - 大型项目建议手动列出源文件,避免 GLOB 的隐式依赖问题
好了,关于 GLOB 的内容就讲到这里。记住一句话:GLOB 是工具,不是银弹。用对了省时省力,用错了坑你没商量。下次你在项目里用 GLOB 的时候,想想我今天说的这几个坑,应该能帮你省下不少调试时间。