文件操作:file命令详解
CMake 里的 file 命令,说白了就是你的瑞士军刀。读写文件、下载东西、打包压缩、路径处理、glob 匹配…… 几乎所有的文件系统操作,它都能干。我最早接触 CMake 时,觉得这命令就是个「文件搬运工」,后来才发现,它其实是构建系统里最容易被低估的利器。
今天我们就把它彻底拆开,一个一个讲清楚。
1. 文件读写:最基础的操作
先看最简单的——读写文件。我个人习惯把项目版本号、构建时间这些信息写到一个头文件里,然后让 C++ 代码去引用。怎么做?用 file(WRITE) 和 file(READ)。
# 写入文件(覆盖)
file(WRITE version.h.in "#define VERSION_MAJOR 1\n")
file(APPEND version.h.in "#define VERSION_MINOR 0\n")
# 读取文件
file(READ version.h.in VERSION_CONTENT)
message("版本内容:${VERSION_CONTENT}")
注意 WRITE 会覆盖已有内容,APPEND 则是追加。我在项目中遇到过一个问题:用 WRITE 写了一个配置文件,结果每次 CMake 重新运行都会覆盖掉用户手动修改的内容。后来我改用 APPEND + 条件判断,才解决了。
file(WRITE) 写临时文件,忘了加 ${CMAKE_CURRENT_BINARY_DIR} 路径,结果文件写到了源码目录里,导致 git 仓库被污染。记住:写文件时,目标路径一定要明确。
2. 文件下载:远程资源获取
如果你的项目需要从网上下载第三方库或数据文件,file(DOWNLOAD) 就是你的好帮手。它支持 HTTP/HTTPS、FTP,还能设置超时、校验哈希。
file(DOWNLOAD
"https://example.com/data.zip"
"${CMAKE_CURRENT_BINARY_DIR}/data.zip"
SHOW_PROGRESS
EXPECTED_MD5 1234567890abcdef1234567890abcdef
TIMEOUT 30
STATUS DOWNLOAD_STATUS
LOG DOWNLOAD_LOG
)
if(NOT DOWNLOAD_STATUS EQUAL 0)
message(FATAL_ERROR "下载失败:${DOWNLOAD_LOG}")
endif()
这里有个小技巧:STATUS 变量会返回一个列表,第一个元素是状态码(0 表示成功),第二个是错误信息。我建议每次都检查一下状态,别指望网络永远可靠。
http_proxy 和 https_proxy,所以提前设好就行。
3. 文件打包:压缩与解压
CMake 3.18 之后,file(ARCHIVE) 命令可以直接创建和提取压缩包。支持 zip、tar、7z 等格式。我在项目中用它来打包构建产物,省去了调用外部工具的麻烦。
# 创建压缩包
file(ARCHIVE_CREATE
OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/output.zip"
PATHS "${CMAKE_CURRENT_BINARY_DIR}/bin"
FORMAT zip
COMPRESSION_LEVEL 9
)
# 解压
file(ARCHIVE_EXTRACT
INPUT "${CMAKE_CURRENT_BINARY_DIR}/output.zip"
DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/extracted"
)
嗯,这里要注意:ARCHIVE_CREATE 的 PATHS 参数可以指定多个路径,但路径必须是绝对路径或相对于当前源码目录的路径。我踩过坑——用了相对路径,结果 CMake 找不到文件。
| 参数 | 说明 | 示例值 |
|---|---|---|
FORMAT |
压缩格式 | zip, tar, 7z, gzip |
COMPRESSION_LEVEL |
压缩级别(0-9) | 9(最高压缩) |
PATHS |
要打包的文件/目录列表 | ${BIN_DIR}, ${LIB_DIR} |
4. 路径操作:别自己拼字符串
很多新手喜欢用字符串拼接来构造路径,比如 set(MY_PATH "${BASE_DIR}/sub/folder")。这其实很危险——不同操作系统路径分隔符不一样。CMake 提供了 file(RELATIVE_PATH)、file(TO_CMAKE_PATH)、file(TO_NATIVE_PATH) 等命令来处理。
# 获取相对路径
file(RELATIVE_PATH REL_PATH
"/home/user/project"
"/home/user/project/src/main.cpp"
)
# REL_PATH = "src/main.cpp"
# 转换路径格式
file(TO_CMAKE_PATH "C:\\Users\\me\\file.txt" CMAKE_PATH)
# CMAKE_PATH = "C:/Users/me/file.txt"
file(TO_NATIVE_PATH "/home/user/file.txt" NATIVE_PATH)
# 在 Windows 上 = "C:\home\user\file.txt"(取决于当前盘符)
我个人习惯:所有路径变量都用 CMake 风格(正斜杠),只在最后输出或传给外部工具时才转成本地格式。这样能避免很多跨平台问题。
5. Glob 模式匹配:批量找文件
Glob 模式匹配,说白了就是用通配符批量查找文件。最常用的是 file(GLOB) 和 file(GLOB_RECURSE)。
# 查找当前目录下所有 .cpp 文件
file(GLOB SOURCES "src/*.cpp")
# 递归查找所有 .h 文件
file(GLOB_RECURSE HEADERS "include/**/*.h")
# 使用更精确的模式
file(GLOB_RECURSE ALL_FILES
"src/*.cpp"
"src/*.h"
"test/*.cpp"
"test/*.h"
)
这里有个争议点:很多 CMake 最佳实践文档说「不要用 GLOB 来收集源文件」,因为 CMake 不会自动检测新文件。但我在实际项目中,只要配合 CONFIGURE_DEPENDS 参数(CMake 3.12+),就能让构建系统在源文件变化时重新运行 glob。
# 推荐做法:加上 CONFIGURE_DEPENDS
file(GLOB SOURCES CONFIGURE_DEPENDS "src/*.cpp")
* 匹配任意字符(不包括路径分隔符),** 匹配任意目录层级。比如 src/**/*.cpp 会匹配 src 下所有子目录里的 .cpp 文件。
6. 知识体系总览
为了让你更直观地理解 file 命令的各个功能模块,我画了一张图:
7. 综合示例:一个实用的文件处理流程
最后,我分享一个实际项目中的用法。这个例子展示了如何用 file 命令完成「下载依赖 → 解压 → 读取配置 → 生成头文件」的完整流程。
# 1. 下载第三方库
file(DOWNLOAD
"https://example.com/libfoo-1.0.zip"
"${CMAKE_CURRENT_BINARY_DIR}/libfoo.zip"
SHOW_PROGRESS
STATUS DL_STATUS
)
if(NOT DL_STATUS EQUAL 0)
message(FATAL_ERROR "下载 libfoo 失败")
endif()
# 2. 解压
file(ARCHIVE_EXTRACT
INPUT "${CMAKE_CURRENT_BINARY_DIR}/libfoo.zip"
DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/libfoo"
)
# 3. 读取版本文件
file(READ "${CMAKE_CURRENT_BINARY_DIR}/libfoo/version.txt" LIBFOO_VERSION)
string(STRIP "${LIBFOO_VERSION}" LIBFOO_VERSION)
# 4. 生成配置头文件
file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/libfoo_config.h"
"#ifndef LIBFOO_CONFIG_H\n"
"#define LIBFOO_CONFIG_H\n"
"#define LIBFOO_VERSION \"${LIBFOO_VERSION}\"\n"
"#endif\n"
)
message("libfoo 版本:${LIBFOO_VERSION},配置已生成")
你看,整个流程用 file 命令就串起来了,不需要调用外部脚本。这也是我越来越喜欢 CMake 的原因——它自己就能搞定大部分文件操作,何必再引入 Python 或 Shell 呢?
好了,关于 file 命令的核心用法就讲到这里。记住:读写用 WRITE/READ,下载用 DOWNLOAD,打包用 ARCHIVE,路径用 RELATIVE/TO_CMAKE,找文件用 GLOB。每个场景都有对应的工具,别自己造轮子。