7、文件操作:file命令与配置文件模板
文件操作,说白了就是CMake和操作系统打交道的「手」。我刚开始用CMake时,总觉得它就是个构建系统,文件读写这种事交给脚本不就行了?后来在项目中吃了不少亏——跨平台时shell脚本不兼容、Python依赖版本问题、甚至有些嵌入式环境连Python都没有。嗯,从那以后,我老老实实用起了CMake自带的file()命令。
这一章,咱们就把file()命令的常用操作捋一遍。你想想看,一个构建系统如果能自己读写文件、下载依赖、复制资源,那自动化程度能提升多少?
核心观点:file()命令是CMake中文件操作的瑞士军刀。掌握它,你就能在构建过程中完成几乎所有文件级任务。
7.1 文件读写:READ / WRITE / APPEND
这三个子命令是最基础的。我个人的习惯是:能用CMake做的文件操作,绝不用外部脚本。原因很简单——跨平台一致性。
7.1.1 READ — 读取文件内容
语法很简单:
file(READ <filename> <out-var> [OFFSET <offset>] [LIMIT <max-in>] [HEX])
举个例子:
file(READ "version.txt" VERSION_CONTENT)
message("版本内容: ${VERSION_CONTENT}")
# 只读前100个字节
file(READ "config.json" CONFIG_HEADER LIMIT 100)
message("配置头: ${CONFIG_HEADER}")
小技巧:加上HEX参数可以读取二进制文件并转为十六进制字符串。我在调试嵌入式固件时用过这个功能,用来比对两个二进制文件是否完全一致。
7.1.2 WRITE / APPEND — 写入与追加
这两个命令我几乎天天用。区别就一句话:WRITE覆盖写,APPEND追加写。
# 写入文件(覆盖)
file(WRITE "output.txt" "第一行内容\n")
file(WRITE "output.txt" "第二行内容\n") # 覆盖了第一行
# 追加文件
file(APPEND "output.txt" "第三行内容\n") # 追加到第二行后面
注意:WRITE会清空原文件!我曾经在CI脚本里犯过这个错——本意是追加日志,结果用了WRITE,把之前的构建日志全覆盖了。排查了半天才发现是命令用错了。
多行写入也很常见:
file(WRITE "config.h"
"#ifndef CONFIG_H\n"
"#define CONFIG_H\n"
"\n"
"#define VERSION \"${PROJECT_VERSION}\"\n"
"#define BUILD_TIME \"${BUILD_TIME}\"\n"
"\n"
"#endif\n"
)
7.2 文件匹配:GLOB 与 GLOB_RECURSE
GLOB,说白了就是「通配符匹配文件名」。我刚开始用CMake时,总喜欢手动列出每个源文件:
set(SOURCES src/main.cpp src/utils.cpp src/network.cpp ...)
后来项目大了,源文件上百个,每次新增文件都要改CMakeLists.txt,烦不烦?用GLOB就省事多了:
file(GLOB SOURCES "src/*.cpp")
add_executable(myapp ${SOURCES})
GLOB_RECURSE会递归搜索子目录:
file(GLOB_RECURSE ALL_SOURCES "src/**/*.cpp")
重要提醒:GLOB有个坑——它只在cmake配置时执行一次。如果你新增了源文件,CMake不会自动感知。需要重新运行cmake命令。我个人建议:小项目用GLOB方便,大项目还是手动列出源文件更可靠。
我曾经在一个有200+源文件的项目里用了GLOB,结果同事新增文件后忘了重新配置,CI构建一直报链接错误。排查了半小时才发现是GLOB没刷新。从那以后,我在大项目里都改用手动列表了。
7.3 网络操作:DOWNLOAD 与 UPLOAD
这个功能我特别喜欢。以前下载第三方依赖,要么用git submodule,要么写shell脚本。CMake自带的file(DOWNLOAD)就能搞定:
file(DOWNLOAD
"https://example.com/libfoo.zip"
"${CMAKE_BINARY_DIR}/libfoo.zip"
SHOW_PROGRESS
EXPECTED_MD5 abc123...
)
参数说明:
| 参数 | 作用 |
|---|---|
| SHOW_PROGRESS | 显示下载进度条 |
| EXPECTED_MD5 | 校验MD5,防止下载损坏 |
| EXPECTED_HASH | 支持SHA256等更安全的哈希 |
| TIMEOUT | 超时时间(秒) |
| INACTIVITY_TIMEOUT | 无数据传输超时 |
经验之谈:下载依赖时,一定要加哈希校验。我遇到过好几次网络不稳定导致下载文件损坏,构建时出现莫名其妙的编译错误。加上EXPECTED_MD5或EXPECTED_HASH,CMake会自动校验,不匹配就报错重试。
UPLOAD用得少一些,但上传构建产物到服务器时很实用:
file(UPLOAD
"${CMAKE_BINARY_DIR}/myapp.exe"
"https://artifactory.example.com/releases/"
SHOW_PROGRESS
)
7.4 复制与重命名:COPY / RENAME
构建过程中经常需要复制文件。比如把生成的库文件复制到输出目录:
file(COPY "lib/libfoo.so" DESTINATION "${CMAKE_INSTALL_PREFIX}/lib/")
COPY支持复制整个目录:
file(COPY "assets/" DESTINATION "${CMAKE_BINARY_DIR}/assets/"
FILES_MATCHING PATTERN "*.png" PATTERN "*.jpg")
RENAME就是重命名文件:
file(RENAME "old_name.txt" "new_name.txt")
注意:RENAME会直接修改文件系统,没有撤销操作。我在CI脚本里用过一次,结果把源文件重命名了,导致后续构建失败。建议先COPY再RENAME副本。
7.5 配置文件模板:configure_file()
这个功能,说白了就是「用CMake变量替换文件中的占位符」。我几乎每个项目都会用到。
假设你有一个模板文件config.h.in:
#ifndef CONFIG_H
#define CONFIG_H
#define PROJECT_NAME "@PROJECT_NAME@"
#define VERSION_MAJOR @PROJECT_VERSION_MAJOR@
#define VERSION_MINOR @PROJECT_VERSION_MINOR@
#define BUILD_TYPE "@CMAKE_BUILD_TYPE@"
#endif
在CMakeLists.txt中:
configure_file(
"config.h.in"
"${CMAKE_BINARY_DIR}/config.h"
@ONLY
)
CMake会把@VAR@替换成对应的CMake变量值。生成后的config.h:
#define PROJECT_NAME "MyApp"
#define VERSION_MAJOR 1
#define VERSION_MINOR 0
#define BUILD_TYPE "Release"
关键参数:
@ONLY:只替换@VAR@格式,不替换${VAR}格式。我建议始终加上,避免意外替换。COPYONLY:只复制文件,不替换任何内容。相当于file(COPY)。NEWLINE_STYLE:控制换行符格式(UNIX/LF、Windows/CRLF等)。跨平台项目必用。
我个人的最佳实践:
# 版本信息模板
configure_file(
"version.h.in"
"${PROJECT_BINARY_DIR}/generated/version.h"
@ONLY
)
# 安装路径配置
configure_file(
"paths.h.in"
"${PROJECT_BINARY_DIR}/generated/paths.h"
@ONLY
NEWLINE_STYLE UNIX
)
# 让编译器能找到生成的文件
target_include_directories(myapp PRIVATE "${PROJECT_BINARY_DIR}/generated")
小技巧:把生成的文件放在${PROJECT_BINARY_DIR}/generated/目录下,和源码分开。这样make clean时自动清理,不会污染源码树。
7.6 其他实用操作
还有一些操作虽然不常用,但关键时刻能救命:
| 命令 | 作用 | 我的使用场景 |
|---|---|---|
| file(REMOVE ...) | 删除文件 | 清理临时构建产物 |
| file(REMOVE_RECURSE ...) | 递归删除目录 | 清理整个构建目录 |
| file(MAKE_DIRECTORY ...) | 创建目录 | 确保输出目录存在 |
| file(SIZE ...) | 获取文件大小 | 检查下载文件是否完整 |
| file(TIMESTAMP ...) | 获取文件时间戳 | 增量构建判断 |
| file(STRINGS ...) | 按行读取字符串 | 解析配置文件 |
举个例子,解析一个简单的配置文件:
# 假设有 config.txt 内容:
# KEY1=VALUE1
# KEY2=VALUE2
file(STRINGS "config.txt" CONFIG_LINES)
foreach(LINE ${CONFIG_LINES})
string(REGEX MATCH "^([^=]+)=(.+)$" _ ${LINE})
if(CMAKE_MATCH_1 AND CMAKE_MATCH_2)
set(${CMAKE_MATCH_1} ${CMAKE_MATCH_2})
message("配置项: ${CMAKE_MATCH_1} = ${CMAKE_MATCH_2}")
endif()
endforeach()
嗯,这一章的内容就这些。文件操作看似基础,但用好了能极大提升构建的自动化程度。我个人建议:能用CMake做的,就别依赖外部工具。跨平台一致性,比什么都重要。
公众号:蓝海资料掘金营,微信deep3321