第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.cppsrc/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.cppwindow.cppdialog.cpp……全部抓出来。

注意:GLOB_RECURSE 默认会进入所有子目录,包括 .git、build 等隐藏目录。如果你不想收集这些目录下的文件,可以用 LIST_DIRECTORIES false 或者配合 REGEX 过滤。

29.3 核心知识点:GLOB 的「一次性」特性

这是最容易被忽略的一点,也是我当年踩过的大坑。

GLOB 只在 CMake 配置阶段执行一次。什么意思呢?你运行cmake ..的时候,GLOB 会扫描目录,把匹配到的文件列表存下来。但如果你之后在源码目录里新增或删除了文件,CMake 并不会自动重新扫描。

我曾经就因为这个吃过亏:在项目里加了一个新.cpp文件,然后直接make,结果链接报错说找不到符号。折腾了半天才发现——CMake 根本不知道我加了新文件,因为它没有重新运行配置。

解决方案有两个:

  1. 手动重新运行 cmake(最直接,但容易忘)
  2. 使用 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 ...) 知识体系 file(GLOB ...) file(GLOB_RECURSE ...) 只扫描当前目录,不递归 支持通配符 * 和 ? 配置阶段执行一次 递归扫描所有子目录 默认包含隐藏目录 可用 REGEX 过滤排除 共同注意:CONFIGURE_DEPENDS 选项 建议:小项目手动列,中项目 GLOB + CONFIGURE_DEPENDS

核心要点回顾:

  • file(GLOB ...) 只扫描当前目录,file(GLOB_RECURSE ...) 递归扫描所有子目录
  • GLOB 只在 CMake 配置阶段执行一次,新增文件后需要重新运行 cmake
  • CMake 3.12+ 支持 CONFIGURE_DEPENDS 选项,让构建时自动检查文件变化
  • REGEX! 可以灵活过滤和排除文件
  • 大型项目建议手动列出源文件,避免 GLOB 的隐式依赖问题

好了,关于 GLOB 的内容就讲到这里。记住一句话:GLOB 是工具,不是银弹。用对了省时省力,用错了坑你没商量。下次你在项目里用 GLOB 的时候,想想我今天说的这几个坑,应该能帮你省下不少调试时间。


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