文件操作: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}")

注意那个 OFFSETLIMIT 参数,它们可以让你只读取文件的某一段。比如读取二进制文件的前 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()
避坑指南:我曾经在 CI 环境里遇到过下载超时的问题。原因是公司内网访问外网需要代理,但 CMake 默认不走代理。解决方案是设置环境变量 http_proxyhttps_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() 命令的核心功能分类:

file() 命令 读写文件 READ / WRITE / APPEND 读取配置、生成代码 支持 OFFSET / LIMIT / HEX 下载文件 DOWNLOAD 支持 TIMEOUT / STATUS 支持 MD5 校验 路径操作 RELATIVE_PATH TO_CMAKE_PATH TO_NATIVE_PATH 目录操作 GLOB / GLOB_RECURSE MAKE_DIRECTORY

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() 能不能搞定——大概率能。


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