7、文件操作:file命令与配置文件模板

文件操作,说白了就是CMake和操作系统打交道的「手」。我刚开始用CMake时,总觉得它就是个构建系统,文件读写这种事交给脚本不就行了?后来在项目中吃了不少亏——跨平台时shell脚本不兼容、Python依赖版本问题、甚至有些嵌入式环境连Python都没有。嗯,从那以后,我老老实实用起了CMake自带的file()命令。

这一章,咱们就把file()命令的常用操作捋一遍。你想想看,一个构建系统如果能自己读写文件、下载依赖、复制资源,那自动化程度能提升多少?

核心观点:file()命令是CMake中文件操作的瑞士军刀。掌握它,你就能在构建过程中完成几乎所有文件级任务。

file() 命令知识体系 file() 命令 读写操作 网络下载 文件匹配 READ WRITE APPEND DOWNLOAD UPLOAD GLOB GLOB_RECURSE 复制/重命名 configure_file() 模板 其他操作

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_MD5EXPECTED_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脚本里用过一次,结果把源文件重命名了,导致后续构建失败。建议先COPYRENAME副本。

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