20、源文件管理:aux_source_directory、file(GLOB ...)、源文件分组、生成器表达式过滤

说实话,CMake 里源文件管理这件事,看着简单,坑却不少。我刚用 CMake 那会儿,习惯把所有 .cpp 文件一个一个列出来,像这样:

add_executable(myapp main.cpp utils.cpp network.cpp ...)

项目小的时候还行。等文件一多,比如几十个源文件,每次新增文件都得改 CMakeLists.txt,烦不烦?嗯,今天我们就聊聊怎么优雅地管理源文件。

20.1 aux_source_directory:自动收集源文件

aux_source_directory 是 CMake 提供的一个命令,能自动扫描指定目录下的所有源文件。用法很简单:

aux_source_directory(${CMAKE_CURRENT_SOURCE_DIR}/src SRC_LIST)
add_executable(myapp ${SRC_LIST})

它会将 src/ 目录下所有 .c.cpp 等源文件收集到变量 SRC_LIST 中。

注意: 这个命令有个限制——它只扫描一次,在 CMake 配置时执行。如果你在构建过程中新增了源文件,它不会自动更新。需要重新运行 cmake 命令。

我个人习惯是,小项目用 aux_source_directory 省事。但大项目嘛……我建议你往下看。

20.2 file(GLOB ...):更灵活的源文件收集

file(GLOB ...)aux_source_directory 灵活得多。你可以指定通配符,比如只收集 .cpp 文件,或者排除某些文件。

file(GLOB SRC_LIST "src/*.cpp" "src/*.c")
add_executable(myapp ${SRC_LIST})

还可以递归搜索子目录:

file(GLOB_RECURSE SRC_LIST "src/**/*.cpp")

这里有个坑,我曾经踩过:file(GLOB) 也是在配置时执行的。如果你新增了文件,CMake 不会自动感知。解决办法是显式声明依赖:

file(GLOB SRC_LIST "src/*.cpp")
set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "src/*.cpp")

这样,当 src/ 目录下的 .cpp 文件有变化时,CMake 会自动重新配置。

小技巧: 我个人更推荐用 file(GLOB ...) 配合 CMAKE_CONFIGURE_DEPENDS,既灵活又可靠。

20.3 源文件分组:source_group

你有没有遇到过这种情况:IDE 里所有源文件都堆在一起,找起来特别费劲?source_group 就是用来解决这个问题的。它可以把源文件按目录结构分组,在 Visual Studio、Xcode 等 IDE 中显示为文件夹。

source_group("Source Files" FILES ${SRC_LIST})
source_group("Header Files" FILES ${HEADER_LIST})

更高级的用法是按目录自动分组:

file(GLOB_RECURSE SRC_LIST "src/**/*.cpp")
foreach(FILE ${SRC_LIST})
    # 获取文件相对于 src/ 的路径
    file(RELATIVE_PATH REL_PATH ${CMAKE_CURRENT_SOURCE_DIR}/src ${FILE})
    # 提取目录部分
    get_filename_component(DIR ${REL_PATH} DIRECTORY)
    # 如果文件在子目录中,则创建分组
    if(DIR)
        source_group("Source\\${DIR}" FILES ${FILE})
    endif()
endforeach()

嗯,这段代码看着有点绕。说白了,就是把 src/utils/network.cpp 放到 Source\utils 分组里。IDE 里看起来就像文件夹一样。

20.4 生成器表达式过滤:精细控制源文件

生成器表达式(Generator Expressions)是 CMake 里一个非常强大的特性。它可以在构建时(而不是配置时)根据条件过滤源文件。语法是 $<condition:value>

举个例子,我想在 Debug 模式下编译 debug_utils.cpp,但在 Release 模式下排除它:

file(GLOB SRC_LIST "src/*.cpp")
list(APPEND SRC_LIST "$<$<CONFIG:Debug>:src/debug_utils.cpp>")
add_executable(myapp ${SRC_LIST})

这里 $<CONFIG:Debug> 是一个布尔表达式,当配置为 Debug 时值为 1,否则为 0。然后 $<1:src/debug_utils.cpp> 会展开为 src/debug_utils.cpp$<0:...> 则展开为空。

更复杂的场景:我想根据平台选择源文件。

file(GLOB SRC_LIST "src/*.cpp")
list(APPEND SRC_LIST
    "$<$<PLATFORM_ID:Windows>:src/win_specific.cpp>"
    "$<$<PLATFORM_ID:Linux>:src/linux_specific.cpp>"
)
add_executable(myapp ${SRC_LIST})

生成器表达式还能嵌套使用。比如:

# 在 Debug 模式下,且平台是 Windows 时,才包含某个文件
list(APPEND SRC_LIST
    "$<$<AND:$<CONFIG:Debug>,$<PLATFORM_ID:Windows>>:src/win_debug.cpp>"
)
核心要点: 生成器表达式是在构建时求值的,所以它比 if() 命令更灵活。你可以在同一个目标中,根据不同的构建配置包含不同的源文件。

20.5 知识体系图

下面这张图总结了本章的核心内容:

源文件管理核心知识体系 源文件管理 aux_source_directory 自动扫描目录 file(GLOB ...) 通配符匹配 source_group IDE分组显示 生成器表达式 构建时条件过滤 只扫描一次 小项目适用 GLOB_RECURSE CMAKE_CONFIGURE_DEPENDS 按目录分组 IDE友好 CONFIG:Debug/Release PLATFORM_ID

20.6 实战建议

说了这么多,到底该怎么选?我根据自己的经验,给你几个建议:

场景 推荐方案 理由
小项目(< 20个源文件) aux_source_directory 简单直接,够用
中等项目(20-100个源文件) file(GLOB ...) + CMAKE_CONFIGURE_DEPENDS 灵活,支持通配符
大型项目(> 100个源文件) file(GLOB_RECURSE ...) + source_group 自动递归,IDE分组清晰
需要条件编译 生成器表达式 构建时过滤,最灵活
避坑指南: 我曾经在一个项目里用 file(GLOB_RECURSE ...) 不加任何过滤,结果把测试文件、第三方库的源文件全扫进来了。编译报错一堆。后来我加了 EXCLUDE 参数才搞定。记住:GLOB_RECURSE 会扫描所有子目录,一定要小心。

好了,源文件管理这块就聊到这儿。记住一个原则:选择最适合你项目规模的方案,不要过度设计。小项目用 aux_source_directory 完全没问题,大项目再用 file(GLOB ...) 和生成器表达式也不迟。


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