7、CMake中的文件操作:file()命令详解(读写、下载、压缩、遍历)、configure_file()
文件操作,说白了就是CMake和操作系统打交道的那双手。你想想看,一个项目从源码到安装包,中间要经历多少文件层面的折腾?读取配置文件、下载依赖库、打包资源文件、遍历源文件列表……这些活儿,file()命令几乎全包了。
我个人习惯把file()比作瑞士军刀——功能多,但每个功能都有自己专属的“刀刃”。今天咱们就把这把刀拆开,一个一个看明白。
7.1 file()命令的“十八般武艺”
file()命令在CMake里是个多面手。它的基本语法很简单:
file(子命令 参数...)
不同的子命令干不同的事。我把它分成四大类:
| 类别 | 子命令 | 一句话说明 |
|---|---|---|
| 读写操作 | READ、WRITE、APPEND、STRINGS | 读文件、写文件、追加内容、读取字符串行 |
| 文件系统 | COPY、RENAME、REMOVE、MAKE_DIRECTORY | 复制、重命名、删除、创建目录 |
| 遍历与查找 | GLOB、GLOB_RECURSE | 匹配文件路径,递归遍历子目录 |
| 网络与压缩 | DOWNLOAD、UPLOAD、ARCHIVE_EXTRACT | 下载文件、上传文件、解压归档 |
嗯,这里要注意:file()是在配置阶段执行的,不是构建阶段。也就是说,当你运行cmake ..的时候,这些操作就已经发生了。
7.2 读写操作:最常用的基本功
先说说读写。我在项目中经常需要生成一些配置文件,比如版本号、编译时间戳之类的。
7.2.1 读取文件内容
file(READ "version.txt" VERSION_CONTENT)
message("版本内容: ${VERSION_CONTENT}")
如果你只想按行读取,用STRINGS更合适:
file(STRINGS "config.ini" CONFIG_LINES)
foreach(line ${CONFIG_LINES})
message("配置行: ${line}")
endforeach()
我曾经踩过一个坑:读取大文件时,READ会把整个文件塞进一个变量里,内存一下子就上去了。后来我改用STRINGS配合LIMIT_INPUT参数,只读前几行,问题就解决了。
7.2.2 写入与追加
file(WRITE "output.txt" "第一行内容\n")
file(APPEND "output.txt" "追加的第二行内容\n")
这里有个细节:WRITE会覆盖已有文件,而APPEND是在末尾追加。我建议你在写构建脚本时,先用WRITE创建文件头,再用APPEND逐步添加内容。这样代码更清晰。
WRITE和APPEND不会自动创建目录。你需要先用file(MAKE_DIRECTORY ...)把目录建好。
7.3 遍历文件:GLOB与GLOB_RECURSE
这是file()命令里最常用的功能之一。说白了,就是帮你自动收集源文件列表,省得你一个一个手动写。
file(GLOB SOURCES "src/*.cpp")
file(GLOB_RECURSE ALL_SOURCES "src/**/*.cpp")
GLOB只匹配当前目录,GLOB_RECURSE会递归进子目录。我个人习惯用GLOB_RECURSE,因为项目大了以后,目录结构经常调整,递归匹配能少改很多CMakeLists.txt。
但是——这里有个大坑。我曾经在一个项目里用GLOB_RECURSE收集源文件,结果新增了一个cpp文件后,重新运行cmake --build居然没编译它!
为什么会这样?因为GLOB的结果是在配置阶段缓存的。如果你不改CMakeLists.txt,CMake不会重新执行GLOB。解决方案有两个:
- 每次新增文件后,手动重新运行
cmake .. - 用
CMAKE_CONFIGURE_DEPENDS告诉CMake监控目录变化
file(GLOB_RECURSE SOURCES "src/*.cpp")
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "src/")
7.4 下载与压缩:网络操作也不在话下
你想想看,如果你的项目需要从网上下载一个第三方库,难道要写个脚本?不用,file(DOWNLOAD ...)就能搞定。
file(DOWNLOAD "https://example.com/lib.zip" "${CMAKE_BINARY_DIR}/lib.zip"
SHOW_PROGRESS
EXPECTED_MD5 1234567890abcdef)
这里我特别想强调EXPECTED_MD5参数。我曾经有一次下载依赖库,结果服务器上的文件被替换了,编译到一半报错,排查了半天才发现是下载的文件不对。加上MD5校验后,文件不对就直接报错,省心多了。
解压操作也很直接:
file(ARCHIVE_EXTRACT INPUT "lib.zip"
DESTINATION "${CMAKE_BINARY_DIR}/third_party")
支持的格式包括zip、tar、tar.gz、tar.bz2等。嗯,这里要注意:ARCHIVE_EXTRACT是CMake 3.18才引入的,如果你用的CMake版本比较老,可能需要用execute_process调用系统命令来解压。
7.5 configure_file():模板文件的魔法
这个命令我太喜欢了。它能把CMake变量直接嵌入到文件中,说白了就是模板引擎。
configure_file("config.h.in" "${PROJECT_BINARY_DIR}/config.h")
假设你的config.h.in内容是这样的:
#define VERSION_MAJOR @PROJECT_VERSION_MAJOR@
#define VERSION_MINOR @PROJECT_VERSION_MINOR@
#define BUILD_TIMESTAMP "@BUILD_TIMESTAMP@"
经过configure_file()处理后,@PROJECT_VERSION_MAJOR@会被替换成实际的版本号。我在项目中经常用它来生成版本头文件、配置文件,甚至一些脚本文件。
这里有两个模式:
- @VAR@ 模式:只替换被@包围的变量,其他内容原样保留
- ${VAR} 模式:会替换所有CMake变量引用,但需要加
@ONLY参数来禁用
configure_file("config.h.in" "config.h" @ONLY)
${...},而且不想被CMake解析,一定要加@ONLY。否则CMake会把所有${...}都尝试替换,很可能报错。
我曾经接手过一个项目,模板文件里既有@VAR@又有${VAR},结果configure_file()把${VAR}也替换了,导致生成的配置文件里全是乱码。排查了半天才发现是@ONLY没加。
7.6 知识体系总览
说了这么多,咱们用一张图把file()和configure_file()的关系理清楚:
7.7 实战经验总结
最后,我把自己这些年用file()和configure_file()的一些心得整理一下:
- 能用
configure_file()就别手动拼接字符串——模板替换更清晰,也更容易维护 - 下载文件一定要加MD5校验——我曾经因为网络问题下载了损坏的文件,排查了一整天
- GLOB结果不可靠——如果你用GLOB,记得加
CMAKE_CONFIGURE_DEPENDS,或者干脆手动列文件 - 文件路径尽量用绝对路径——
${CMAKE_CURRENT_SOURCE_DIR}和${CMAKE_CURRENT_BINARY_DIR}是你的好朋友 - 解压操作注意CMake版本——3.18以下需要自己写
execute_process
嗯,文件操作这块内容其实不难,但细节特别多。你只要记住一点:file()是配置阶段的工具,它帮你准备好构建所需的一切文件。而configure_file()则是把CMake的变量能力延伸到文件层面,让配置生成变得优雅。
下次你写CMakeLists.txt的时候,遇到文件相关的需求,先想想file()能不能搞定。大概率是能的。