文件操作:file()命令读写文件、下载文件、路径操作
说实话,CMake 里的 file() 命令,是我用得最频繁的命令之一。为什么?因为构建过程本质上就是跟文件打交道——读取配置、生成代码、复制资源、下载依赖……这些活儿 file() 全包了。
我记得刚接触 CMake 那会儿,总觉得文件操作是脚本语言的事,CMake 能搞定吗?后来发现,file() 不仅行,而且相当强大。今天咱们就把它的核心用法捋一遍。
1. 读写文件:最基础的操作
读写文件,说白了就是两个方向:把内容读进来,把内容写出去。
读取文件内容
file(READ <filename> <variable> [OFFSET <offset>] [LIMIT <max-in>] [HEX])
这个命令会把文件内容读到一个变量里。我在项目中经常用它来读取版本号文件:
file(READ "version.txt" VERSION_CONTENT)
string(STRIP "${VERSION_CONTENT}" VERSION)
message(STATUS "当前版本: ${VERSION}")
注意那个 OFFSET 和 LIMIT 参数,它们可以让你只读取文件的某一段。比如读取二进制文件的前 16 个字节:
file(READ "header.bin" HEADER_DATA LIMIT 16 HEX)
message(STATUS "文件头: ${HEADER_DATA}")
LIMIT 限制读取量,否则 CMake 会一次性把整个文件塞进内存,构建速度会受影响。
写入文件
file(WRITE <filename> <content>...)
file(APPEND <filename> <content>...)
WRITE 会覆盖文件,APPEND 则是追加内容。我习惯用它们来生成配置文件:
file(WRITE "config.h.in"
"#ifndef CONFIG_H\n"
"#define CONFIG_H\n"
"#define VERSION \"${PROJECT_VERSION}\"\n"
"#define BUILD_TIME \"${BUILD_TIME}\"\n"
"#endif\n"
)
嗯,这里要注意:WRITE 会直接覆盖已有文件,没有确认提示。我曾经手滑写错过路径,把构建目录里的文件覆盖了……从那以后,我写 WRITE 之前都会先检查路径对不对。
2. 下载文件:远程依赖的救星
构建过程中需要下载第三方库?file(DOWNLOAD) 就是干这个的。
file(DOWNLOAD <url> <file> [TIMEOUT <seconds>] [STATUS <result-var>] [EXPECTED_MD5 <md5>])
我个人习惯在下载时加上 EXPECTED_MD5 校验,确保文件没被篡改:
set(THIRD_PARTY_URL "https://example.com/libfoo-1.2.3.tar.gz")
set(THIRD_PARTY_MD5 "a1b2c3d4e5f6...")
file(DOWNLOAD "${THIRD_PARTY_URL}" "${CMAKE_BINARY_DIR}/libfoo.tar.gz"
TIMEOUT 30
EXPECTED_MD5 "${THIRD_PARTY_MD5}"
STATUS DOWNLOAD_STATUS
)
list(GET DOWNLOAD_STATUS 0 STATUS_CODE)
if(NOT STATUS_CODE EQUAL 0)
message(FATAL_ERROR "下载失败,状态码: ${STATUS_CODE}")
endif()
http_proxy 和 https_proxy。
3. 路径操作:别自己拼字符串
很多新手喜欢用字符串拼接来构造路径,比如 ${DIR}/subdir/file.txt。这在 Windows 上会出问题——反斜杠和正斜杠混用,够你头疼的。
file() 提供了专门的路径操作命令:
file(RELATIVE_PATH <variable> <directory> <file>)
file(TO_CMAKE_PATH <path> <variable>)
file(TO_NATIVE_PATH <path> <variable>)
举个例子:
set(ABSOLUTE_PATH "/home/user/project/src/main.cpp")
file(RELATIVE_PATH REL_PATH "/home/user/project" "${ABSOLUTE_PATH}")
message(STATUS "相对路径: ${REL_PATH}") # 输出: src/main.cpp
# Windows 路径转换
file(TO_CMAKE_PATH "C:\\Users\\me\\file.txt" CMAKE_PATH)
message(STATUS "CMake 路径: ${CMAKE_PATH}") # 输出: C:/Users/me/file.txt
你想想看,如果用手动字符串替换来处理路径,得写多少代码?而且容易出错。file(TO_CMAKE_PATH) 一行搞定,跨平台无压力。
4. 目录操作:遍历与创建
有时候需要遍历某个目录下的所有源文件:
file(GLOB_RECURSE SOURCES "src/*.cpp" "src/*.c")
file(GLOB_RECURSE HEADERS "include/**/*.h")
不过我要提醒一句:GLOB_RECURSE 在 CMake 官方文档里是不推荐用于源文件收集的。为什么?因为 CMake 不会自动检测新添加的文件,你需要手动重新运行 cmake。我个人的做法是:
- 小项目(文件少于 20 个):手动列出源文件,清晰可控
- 大项目:用
GLOB_RECURSE配合CONFIGURE_DEPENDS标志
file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS "src/*.cpp")
创建目录也很简单:
file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/output/logs")
这个命令会递归创建所有不存在的目录,相当于 mkdir -p。
5. 知识体系总览
下面这张图总结了 file() 命令的核心功能分类:
6. 实战:一个完整的文件操作示例
最后,咱们把今天学的串起来,写一个实际场景:下载一个 JSON 配置文件,读取它,然后生成 C++ 头文件。
# 1. 下载配置文件
set(CONFIG_URL "https://example.com/config.json")
set(CONFIG_FILE "${CMAKE_BINARY_DIR}/config.json")
file(DOWNLOAD "${CONFIG_URL}" "${CONFIG_FILE}"
TIMEOUT 10
STATUS DL_STATUS
)
# 2. 检查下载是否成功
list(GET DL_STATUS 0 DL_CODE)
if(NOT DL_CODE EQUAL 0)
message(FATAL_ERROR "下载配置文件失败")
endif()
# 3. 读取 JSON 内容
file(READ "${CONFIG_FILE}" CONFIG_JSON)
# 4. 提取版本号(假设 JSON 格式: {"version": "2.1.0"})
string(JSON VERSION GET "${CONFIG_JSON}" version)
message(STATUS "配置版本: ${VERSION}")
# 5. 生成头文件
file(WRITE "${CMAKE_BINARY_DIR}/generated/config.h"
"#pragma once\n"
"#define APP_VERSION \"${VERSION}\"\n"
"#define BUILD_NUMBER \"${BUILD_NUMBER}\"\n"
)
你看,从下载到读取再到生成,file() 一条龙搞定。不需要写脚本,不需要外部工具,CMake 自己就能完成。
- 读写文件用
READ/WRITE/APPEND - 下载文件用
DOWNLOAD,记得加校验 - 路径操作用
RELATIVE_PATH/TO_CMAKE_PATH,别自己拼字符串 - 目录遍历用
GLOB_RECURSE,但注意文件变更检测问题
好了,file() 命令的核心用法就这些。实际项目中,你八成会频繁用到它。尤其是跨平台项目,路径操作和文件下载几乎是标配。下次遇到文件相关的需求,先想想 file() 能不能搞定——大概率能。