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 中。
我个人习惯是,小项目用 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 知识体系图
下面这张图总结了本章的核心内容:
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