第二十章:文件操作

Posted by lili on

许多项目在构建过程中需要操作文件和目录。虽然这些操作从简单到相当复杂不等,但更常见的任务包括:

  • 构造路径或提取路径的组成部分。
  • 从目录中获取文件列表。
  • 复制文件。
  • 从字符串内容生成文件。
  • 从另一个文件的内容生成文件。
  • 读取文件的内容。
  • 计算文件的校验和或哈希值。

CMake提供了许多与文件和目录操作相关的功能。在某些情况下,可能有多种实现相同目标的方式,因此了解不同选择并有效地使用它们是很有用的。其中一些功能经常被误用,有些是因为在线教程和示例中普遍存在这种误用,导致人们认为这是正确的做法。本章讨论了一些更为常见的反模式。

CMake的文件相关功能大部分由file()命令提供,还有一些其他命令提供了更适合某些情况或提供了相关的辅助功能。CMake的命令模式,是上一章介绍的,也提供了许多与文件相关的功能,与file()提供的大部分重叠,但它覆盖了一组与file()不同的情况,而不是在大多数情况下是替代方案。

20.1 路径操作

文件处理的最基本部分之一是操作文件名和路径。项目通常需要从完整路径中提取文件名、文件后缀等,或者在绝对路径和相对路径之间进行转换。在CMake 3.19及更早版本中,这种功能分布在两个命令get_filename_component()和file()之间。这两个命令有一些重叠,存在一些不一致性。CMake 3.20引入了一个新命令,cmake_path(),它取代了这两个命令的大部分路径处理功能。它提供了一个更一致、更可预测的接口。

20.1.1 cmake_path()

cmake_path()的官方文档非常全面。它涵盖了所使用的概念,按照逻辑、基于任务的分组呈现了各种子命令,并提供了大量示例。鼓励读者研究其中呈现的材料,以深入了解该命令。这里只描述了一些常见任务的关键概念和子命令。

cmake_path()命令永远不会访问底层文件系统。它只在语法上操作路径,因此不知道符号链接或路径的存在。它使用了一个明确定义的路径结构,其中斜杠始终用作目录分隔符,而不考虑主机或目标平台。只有在主机平台支持时,才支持驱动器号或映射的驱动器名称(例如C:或//myserver)。

以下示例路径和表格说明了所使用的术语:

C:/one/two/start.middle.end

可以使用 GET 子命令来检索上述路径组件之一(将 <COMP> 替换为上述表格中的一项):

cmake_path(GET pathVar <COMP> [LAST_ONLY] outVar)

仅当 <COMP> 为 EXTENSION 或 STEM 时才可以提供 LAST_ONLY。默认情况下,EXTENSION 从 FILENAME 的最左边的点 (.) 字符开始,但 LAST_ONLY 会改变这一行为,改为使用最右边的点字符。STEM 是不带有 EXTENSION 的 FILENAME,LAST_ONLY 关键字同样会改变此情况下的 EXTENSION 的含义。

set(path "a.b.c")
cmake_path(GET path EXTENSION result) # .b.c
cmake_path(GET path EXTENSION LAST_ONLY result) # .c
cmake_path(GET path STEM result) # a
cmake_path(GET path STEM LAST_ONLY result) # a.b

正如上面的示例所示,需要注意 pathVar 是一个保存路径的变量的名称,而不是一个字符串:

# WRONG: Cannot use a string for the path
cmake_path(GET "/some/path/example" FILENAME result)

# Correct, but can be improved (see below)
set(path "/some/path/example")
cmake_path(GET path FILENAME result)

虽然使用 set() 创建路径变量是允许的,但 cmake_path() 有一个专门的 SET 子命令,它更加健壮,并具有其他功能:

cmake_path(SET pathVar [NORMALIZE] input)

使用 cmake_path(SET) 而不是 set() 的主要优势在于,前者会自动将本地路径转换为 cmake 风格路径,其他 cmake_path() 子命令所期望的格式。另一个优势是能够对路径进行规范化。CMake 文档描述了 NORMALIZE 关键字的正式规则,但基本上它意味着通过解析像 . 和 .. 这样的内容,将多个连续的路径分隔符 (/) 折叠为单个分隔符,以及一些其他特殊情况来简化路径。需要注意的是,因为 cmake_path() 从不访问文件系统,所以它在规范化中不会解析符号链接。

cmake_path(SET path NORMALIZE "/some//path/xxx/../example")
# The path variable now holds the value: /some/path/example

还可以查询路径以查看它是否具有特定的路径组件:

cmake_path(<OP> pathVar outVar)

<OP> 的有效值包括 GET 的所有相同 <COMP> 值,但前缀为 HAS_(例如 HAS_EXTENSION、HAS_RELATIVE_PART)。此外,IS_ABSOLUTE 和 IS_RELATIVE 也是 <OP> 的受支持值,但它们具有一些不太明显的行为方面需要注意。IS_ABSOLUTE 在技术上意味着路径明确地引用某个位置,而不需要引用某个相对点。这主要与 Windows 主机和路径相关,因为路径必须从根 / 开始,还必须具有驱动器号才能被视为绝对路径。在非 Windows 主机平台上,具有驱动器号的路径被视为格式错误。因此,相同的路径可以在不同的主机平台上产生不同的结果。如果在 Windows 主机上进行交叉编译并测试用于非 Windows 目标平台的路径,则特别危险。

下表显示了不同情况下 cmake_path(IS_ABSOLUTE pathVar result) 的结果:

在所有平台上,IS_RELATIVE 的结果始终与 IS_ABSOLUTE 的结果相反,因此它表现出相同的平台相关差异。

有用于转换为绝对或相对路径的子命令:

cmake_path(RELATIVE_PATH pathVar
 [BASE_DIRECTORY baseDir]
 [OUTPUT_VARIABLE outVar]
)

cmake_path(ABSOLUTE_PATH pathVar [NORMALIZE]
 [BASE_DIRECTORY baseDir]
 [OUTPUT_VARIABLE outVar]
)

相对路径被认为是相对于指定的 baseDir(如果给定)或否则相对于 CMAKE_CURRENT_SOURCE_DIR。如果没有给出 outVar,则 pathVar 将在原地修改。NORMALIZE 关键字具有将结果路径归一化的常规效果。也可以使用另一个子命令显式地将路径归一化:

cmake_path(NORMAL_PATH pathVar [OUTPUT_VARIABLE outVar])

在 CMake 的所有文件处理中,大多数情况下,项目可以在所有平台上使用斜杠作为目录分隔符,而 CMake 会根据需要自动将其转换为本地路径。然而,偶尔情况下,项目可能需要显式地在 CMake 和本地路径之间进行转换。一个这样的例子是在处理自定义命令时需要将路径传递给需要本地路径的脚本。对于这些情况,可以使用 NATIVE_PATH 子命令:

cmake_path(NATIVE_PATH pathVar [NORMALIZE] outVar)

还有其他 cmake_path() 子命令,但上面介绍的这些覆盖了最常见的用例。

20.1.2. 旧命令

对于必须支持 CMake 3.19 或更早版本的项目,无法使用 cmake_path() 命令。尽管如此,相同的功能或多或少地可以通过更旧的 get_filename_component() 和 file() 命令来实现,尽管它们的语法形式不够一致。与不会访问文件系统的 cmake_path() 不同,这两个命令也可能会在解析路径时访问文件系统。

在 CMake 3.19 及更早版本中,执行与路径相关的操作的主要方法是使用 get_filename_component() 命令。它有三种不同的形式。第一种形式允许提取路径或文件名的不同部分,类似于 cmake_path(GET) 提供的功能:

get_filename_component(outVar input component [CACHE])

调用的结果存储在由 outVar 指定的变量中。要从输入中提取的组件由 component 指定,它必须是以下之一:

  • DIRECTORY:提取输入的路径部分,不包括文件名。在 CMake 2.8.12 之前,此选项曾被命名为 PATH,现在仍被接受为 DIRECTORY 的同义词,以保持与旧版本的兼容性。
  • NAME:提取完整的文件名,包括任何扩展名。这实际上只是丢弃输入的目录部分。
  • NAME_WE:仅提取基本文件名。这类似于 NAME,但仅提取文件名直到第一个“.”之前的部分。
  • NAME_WLE:类似于 NAME_WE,但提取直到最后一个“.”之前的名称。此选项仅适用于 CMake 3.14 或更高版本。
  • EXT:这是 NAME_WE 的补充。它仅从第一个“.”开始提取文件名的扩展名部分。这可以看作是文件名中最长的扩展名。
  • LAST_EXT:类似于 EXT,但返回最短的扩展名(即从最后一个“.”开始的文件名部分)。此选项仅适用于 CMake 3.14 或更高版本。

CACHE 关键字是可选的。如果存在,结果将存储为缓存变量而不是常规变量。通常,不希望将结果存储在缓存中,因此通常不需要 CACHE 关键字。

set(input /some/path/foo.bar.txt)
get_filename_component(path1 ${input} DIRECTORY) # /some/path
get_filename_component(path2 ${input} PATH) # /some/path
get_filename_component(fullName ${input} NAME) # foo.bar.txt
get_filename_component(baseNameShort ${input} NAME_WE) # foo
get_filename_component(baseNameLong ${input} NAME_WLE) # foo.bar
get_filename_component(extensionLong ${input} EXT) # .bar.txt
get_filename_component(extensionShort ${input} LAST_EXT) # .txt

get_filename_component() 的第二种形式用于获取绝对路径:

get_filename_component(outVar input component [BASE_DIR baseDir] [CACHE])

在这种形式中,input 可以是相对路径,也可以是绝对路径。如果提供了 BASE_DIR,那么相对路径将被解释为相对于 baseDir 而不是当前源目录(即 CMAKE_CURRENT_SOURCE_DIR)。如果 input 已经是绝对路径,则会忽略 BASE_DIR。与 cmake_path() 不同,此命令可以访问文件系统并解析符号链接。component 参数控制存储在 outVar 中的路径的符号链接处理方式:

  • ABSOLUTE:计算 input 的绝对路径,但不解析符号链接。
  • REALPATH:计算 input 的绝对路径,并解析符号链接。

file() 命令提供了反向操作,将绝对路径转换为相对路径:

file(RELATIVE_PATH outVar relativeToDir input)

CMake 3.19 还添加了一个 file() 子命令,它本质上相当于 get_filename_component() 的 REALPATH 操作:

file(REAL_PATH input outVar
 [BASE_DIRECTORY baseDir]
 [EXPAND_TILDE]
 # Requires CMake 3.21 or later
)

在 CMake 3.21 或更高版本中,当给出 EXPAND_TILDE 关键字并且 input 以波浪号(~)开头时,波浪号将被替换为用户家目录的路径。这模仿了大多数 Unix shell 的行为。

不幸的是,file(REAL_PATH) 子命令引入了一些不一致性:

  • 输入和 outVar 参数的顺序与对应的 RELATIVE_PATH 操作不同。
  • file() 命令使用 REAL_PATH(注意下划线),而 get_filename_component() 使用 REALPATH。
  • 可选的基础目录关键字对于 file(REAL_PATH) 命令被命名为 BASE_DIRECTORY,而对于 get_filename_component() 命令则命名为 BASE_DIR。

以上不一致性可能会使得这些命令的使用有些容易出错,因此需要额外小心。

以下示例演示了这些 file() 子命令的用法:

set(basePath /base)
set(fooBarPathset /base/foo/bar)
set(otherPath /other/place)

file(RELATIVE_PATH fooBar ${basePath} ${fooBarPath})
file(RELATIVE_PATH other ${basePath} ${otherPath})

file(REAL_PATH ${other} otherReal BASE_DIRECTORY ${basePath})

在上述代码块的末尾,变量具有以下值:

fooBar = foo/bar
other = ../other/place
otherReal = /other/place

get_filename_component() 命令的第三种形式是方便地提取完整命令行的部分(没有相应的 cmake_path() 命令):

get_filename_component(progVar input PROGRAM [PROGRAM_ARGS argVar] [CACHE])

使用这种形式,假定 input 是一个可能包含参数的命令行。CMake 将提取指定命令行将要调用的可执行文件的完整路径,必要时使用 PATH 环境变量来解析可执行文件的位置,并将结果存储在 progVar 中。如果给出了 PROGRAM_ARGS,则将命令行参数集合存储为由 argVar 命名的变量中。CACHE 关键字的含义与 get_filename_component() 的其他形式相同。

file() 命令提供了另外两种形式,用于在平台原生格式和 CMake 格式之间转换路径:

file(TO_NATIVE_PATH input outVar)
file(TO_CMAKE_PATH input outVar)

TO_NATIVE_PATH 形式将 input 转换为主机平台的原生路径。这相当于确保使用正确的目录分隔符(在 Windows 上为反斜杠,在其他所有地方为正斜杠)。TO_CMAKE_PATH 形式将 input 中的所有目录分隔符转换为正斜杠。这是 CMake 用于所有平台路径的表示。input 也可以是与平台的 PATH 环境变量兼容的形式中指定的路径列表。所有的冒号分隔符都被替换为分号,从而将类似于 PATH 的输入转换为 CMake 路径列表。

# Unix 示例
set(customPath /usr/local/bin:/usr/bin:/bin)
file(TO_CMAKE_PATH ${customPath} outVar)
# outVar = /usr/local/bin;/usr/bin;/bin

20.2. 文件复制

在配置阶段或构建过程中需要复制文件的需求相对常见。由于对大多数用户来说,复制文件是一个很熟悉的任务,因此新的CMake开发者自然会使用他们已经了解的相同方法来实现文件复制。不幸的是,这通常会导致使用特定于平台的shell命令与 add_custom_target() 和 add_custom_command() 结合使用,有时还会出现依赖问题,需要开发者多次运行CMake和/或手动按特定顺序构建目标。

在几乎所有情况下,CMake提供了比这种特定于平台的方法更好的替代方案。在本节中,将介绍一些复制文件的技术。一些旨在满足特定需求,而另一些则旨在更通用,可以在各种情况下使用。所介绍的所有方法在所有平台上都完全相同。

在配置时间复制文件的最有用命令之一,不幸的是,名称并不那么直观。configure_file() 命令允许将单个文件从一个位置复制到另一个位置,可选地在此过程中执行CMake变量替换。复制立即执行,因此它是一个配置时操作。命令的略微简化形式如下:

configure_file(source destination
 [COPYONLY | @ONLY] [ESCAPE_QUOTES]
 # See below for availability
 [NO_SOURCE_PERMISSIONS | USE_SOURCE_PERMISSIONS |
 FILE_PERMISSIONS permissions...]
)

源必须是一个现有文件,可以是绝对路径或相对路径,后者相对于当前源目录(即 CMAKE_CURRENT_SOURCE_DIR)。目标可以是一个现有目录或要复制到的文件名。使用目录名称是有风险的,因为如果没有指定名称的目录,则将创建该名称的文件。目标可以包括路径,路径可以是绝对的或相对的。如果目标不是绝对路径,则解释为相对于当前二进制目录(即 CMAKE_CURRENT_BINARY_DIR)。如果目标路径的任何部分不存在,CMake 将尝试在调用的过程中创建缺失的目录。请注意,通常会看到项目将 CMAKE_CURRENT_SOURCE_DIR 或 CMAKE_CURRENT_BINARY_DIR 作为源和目标路径的一部分,但这只是增加了不必要的混乱,应该避免使用。

默认情况下,目标文件的权限与源文件相同。从 CMake 3.19 开始,可以使用 NO_SOURCE_PERMISSIONS 选项,并且目标文件将可以被所有人读取,只能由用户写入,而且不可执行。从 CMake 3.20 开始,可以使用 USE_SOURCE_PERMISSIONS 或 FILE_PERMISSIONS。前者已经是默认行为,但可以指定以清楚地指示意图。FILE_PERMISSIONS 允许对分配给目标的权限进行完全控制。这三个与权限相关的关键字也被 file(COPY) 和 file(GENERATE) 命令支持。下面讨论了这些命令时包含了权限的指定示例。

如果源文件被修改,构建将考虑目标已过时,并将自动重新运行 cmake。如果配置和生成时间不稳定,并且源文件经常被修改,这可能会成为开发者的一个头疼之源。因此,最好只在不太经常需要更改的文件上使用 configure_file()。

在执行复制时,configure_file() 有能力替换CMake变量。没有 COPYONLY 或 @ONLY 选项,源文件中类似于使用CMake变量的任何内容(即具有形式 ${someVar})都将被该变量的值替换。如果没有具有该名称的变量,则将替换为空字符串。形式为 @someVar@ 的字符串也以相同的方式进行替换。以下显示了一些替换示例:

CMakeLists.txt:

set(FOO "String with spaces")
configure_file(various.txt.in various.txt)

various.txt.in:

CMake version: ${CMAKE_VERSION}
Substitution works inside quotes too: "${FOO}"
No substitution without the $ and {}: FOO
Empty ${} specifier gets removed
Escaping has no effect: \${FOO}
@-syntax also supported: @FOO@

various.txt:

CMake version: 3.7.0
Substitution works inside quotes too: "String with spaces"
No substitution without the $ and {}: FOO
Empty specifier gets removed
Escaping has no effect: \String with spaces
@-syntax also supported: String with spaces

ESCAPE_QUOTES 关键字可用于使任何被替换的引号前面加上反斜杠。

CMakeLists.txt:

set(BAR "Some \"quoted\" value")
configure_file(quoting.txt.in quoting.txt)
configure_file(quoting.txt.in quoting_escaped.txt ESCAPE_QUOTES)

quoting.txt.in:

A: @BAR@
B: "@BAR@"

quoting.txt:

A: Some "quoted" value
B: "Some "quoted" value"

quoting_escaped.txt:

A: Some \"quoted\" value
B: "Some \"quoted\" value"

正如上面的示例所示,ESCAPE_QUOTES 选项会导致所有引号进行转义,而不考虑它们的上下文。因此,在复制的文件对空格和引号敏感的情况下,必须要小心执行可能进行的任何替换。

一些文件类型需要保留 ${someVar} 形式而不进行替换。其中一个经典示例是复制 Unix shell 脚本,其中 ${someVar} 是引用 shell 变量的常用方式。在这种情况下,替换可以仅限于 @someVar@ 形式,使用 @ONLY 关键字:

CMakeLists.txt

set(USER_FILE whoami.txt)
configure_file(whoami.sh.in whoami.sh @ONLY)

whoami.sh.in

#!/bin/sh
echo ${USER} > "@USER_FILE@"

whoami.sh

#!/bin/sh
echo ${USER} > "whoami.txt"

替换也可以通过 COPYONLY 关键字完全禁用。如果知道不需要替换,指定 COPYONLY 是个好做法,因为它可以防止不必要的处理和任何意外的替换。

在使用 configure_file() 进行文件名或路径替换时,一个常见的错误是处理空格和引号不当。如果源文件需要用引号括起来一个替换的变量,以便将其视为单个路径或文件名,则会出现这种情况。这就是为什么上面示例中源文件使用 “@USER_FILE@” 而不是 @USER_FILE@ 作为要写入输出的文件名。

${someVar} 或 @someVar@ 形式中的 CMake 变量替换也可以在字符串上执行,不仅仅是在文件上。string(CONFIGURE) 命令提供了等效的功能和选项。当要复制的内容需要比简单的替换更复杂的步骤时,它会很有用:

string(CONFIGURE input outVar [@ONLY] [ESCAPE_QUOTES])

configure_file() 命令使用文件作为输入和输出。string(CONFIGURE) 子命令使用字符串作为输入和输出。CMake 3.18 添加了一种支持使用字符串作为输入和文件作为输出的第三种方法:

file(CONFIGURE
 OUTPUT outFile
 CONTENT inputString
 [@ONLY] [ESCAPE_QUOTES]
 # ... Other rarely used options
)

OUTPUT 和 CONTENT 分别指定输出文件和输入字符串。如果 outFile 是相对路径,则假定它是相对于当前二进制目录。其余选项的含义与 configure_file() 命令完全相同。

在不需要替换的情况下,另一种选择是使用其中一个相关的 file() 子命令。最灵活且成熟的是 COPY 和 INSTALL 形式,它们在所有 CMake 版本中都可用,并支持相同的一组选项:

file(<COPY|INSTALL> fileOrDir1 [fileOrDir2...]
 DESTINATION dir
 [NO_SOURCE_PERMISSIONS | USE_SOURCE_PERMISSIONS |
 [FILE_PERMISSIONS permissions...]
 [DIRECTORY_PERMISSIONS permissions...]]
 [FOLLOW_SYMLINK_CHAIN] # Requires CMake 3.15 or later
 [FILES_MATCHING]
 [ [PATTERN pattern | REGEX regex] [EXCLUDE]
 [PERMISSIONS permissions...] ] [...]
)

多个文件甚至整个目录层次结构都可以复制到指定目录,甚至可以保留符号链接(如果存在)。指定的任何源文件或目录,如果没有绝对路径,则视为相对于当前源目录。同样,如果目标目录不是绝对路径,则将其解释为相对于当前二进制目录。目标目录结构将根据需要创建。

如果源是目录名称,则将其复制到目标中。如果要将目录的内容而不是目录本身复制到目标中,则将斜杠(/)附加到源目录末尾,如下所示:

file(COPY base/srcDir DESTINATION destDir) # --> destDir/srcDir
file(COPY base/srcDir/ DESTINATION destDir) # --> destDir

默认情况下,COPY 形式将导致所有文件和目录保留与从中复制的源相同的权限,而 INSTALL 形式不会保留原始权限。可以使用 NO_SOURCE_PERMISSIONS 和 USE_SOURCE_PERMISSIONS 选项来覆盖这些默认值,或者可以使用 FILE_PERMISSIONS 和 DIRECTORY_PERMISSIONS 选项明确指定权限。权限值基于 Unix 系统支持的权限:

如果在特定平台上无法理解特定权限,则会简单地忽略它。可以(通常)一起列出多个权限。例如,Unix shell 脚本可能会被复制到当前二进制目录,如下所示:

file(COPY whoami.sh
 DESTINATION .
 FILE_PERMISSIONS
 OWNER_READ OWNER_WRITE OWNER_EXECUTE
 GROUP_READ GROUP_EXECUTE
 WORLD_READ WORLD_EXECUTE
)

COPY 和 INSTALL 签名都会保留被复制文件和目录的时间戳。此外,如果源文件与具有相同时间戳的目标文件已经存在,则将被视为已经完成复制,并将被跳过。除了默认权限之外,COPY 和 INSTALL 之间的唯一区别是 INSTALL 形式为每个复制的项目打印状态消息,而 COPY 形式不会。这种区别是因为 INSTALL 形式通常用作以脚本模式运行的 CMake 脚本的一部分,用于安装文件,在其中常见的行为是打印每个安装的文件的名称。

从 CMake 3.15 或更高版本开始,支持 FOLLOW_SYMLINK_CHAIN 关键字。当存在此选项时,要复制/安装的文件列表中的符号链接将递归复制,同时保留符号链接。递归停止时,将像通常一样复制最后的非符号链接文件。在像这样复制或安装符号链接时,所有路径都会被剥离,因此此功能实际上只适用于符号链接指向同一目录中的情况。

考虑在 Linux 上的一组相对标准的库符号链接,例如以下内容:

libMyStuff.so.2.4.3
libMyStuff.so.2 --> libMyStuff.so.2.4.3
libMyStuff.so --> libMyStuff.so.2

如果 libMyStuff.so 被提供给 file(COPY) 或 file(INSTALL) 命令,并且存在 FOLLOW_SYMLINK_CHAIN 选项,那么上述三个符号链接都会被复制/安装,并且相对符号链接将完全保留如上所示。请注意,符号链接只沿着一个方向进行跟踪,没有逻辑来查找链接到所列文件的内容。因此,对于上面的例子,如果只列出了 libMyStuff.so.2,则不会发现 libMyStuff.so 符号链接,因此不会将其复制/安装。

COPY 和 INSTALL 都支持对匹配或不匹配特定通配符模式或正则表达式的文件应用特定逻辑。这可以用于限制复制的文件以及仅为匹配的文件覆盖权限。可以在一个 file() 命令中给出多个模式和正则表达式。最好通过示例来展示用法。

以下示例从 someDir 中复制所有头文件 (.h) 和脚本文件 (.sh),但不复制文件名以 _private.h 结尾的头文件。头文件被赋予与被复制文件相同的权限,而脚本文件被赋予所有者的读取、写入和执行权限。目录结构将得到保留。

file(COPY someDir
 DESTINATION .
 FILES_MATCHING
 REGEX .*_private\\.h EXCLUDE
 PATTERN *.h
 PATTERN *.sh
 PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE
)

如果应该复制整个源但需要仅为匹配的文件子集覆盖权限,则可以省略 FILES_MATCHING 关键字,并且模式和正则表达式仅用于应用权限覆盖。

file(COPY someDir
 DESTINATION .
 # Make Unix shell scripts executable by everyone
 PATTERN *.sh PERMISSIONS
 OWNER_READ OWNER_WRITE OWNER_EXECUTE
 GROUP_READ GROUP_EXECUTE
 WORLD_READ WORLD_EXECUTE
 # Ensure only owner can read/write private key files
 REGEX _dsa\$|_rsa\$ PERMISSIONS
 OWNER_READ OWNER_WRITE
)

对于非常简单的文件复制操作,CMake 3.21 或更高版本提供了一种开发人员可能会发现更易于使用的替代子命令:

file(COPY_FILE source destination
 [RESULT result]
 [ONLY_IF_DIFFERENT]
)

如果给出了 ONLY_IF_DIFFERENT,则如果目标的时间戳与源文件的内容相同,则目标的时间戳不会被更新。通常建议包括此选项,以避免触发对目标依赖项的不必要重建。

如果给出了 RESULT 关键字,则命令的结果将存储在命名变量中。这允许处理继续进行并从错误中恢复。在成功时,outVar 中存储的值为 0,否则为错误消息。如果没有 RESULT 关键字,则在遇到任何错误时处理将停止。

CMake 还提供了进一步的方法来复制文件和目录。虽然 configure_file() 和 file() 主要用于配置时或在安装时的 CMake 脚本中使用,但 CMake 的命令模式通常用于在构建时复制文件和目录。命令模式是将内容作为 add_custom_target() 和 add_custom_command() 规则的一部分进行复制的首选方式,因为它提供了平台独立性(参见第 19.5 节,“平台独立命令”)。有三个与复制相关的命令,第一个用于复制单个文件:

cmake -E copy file1 [file2...] destination

如果只提供一个源文件,则目标将被视为要复制到的文件名,除非它命名了一个已存在的目录。当目标是现有目录时,源文件将被复制到其中。此行为与大多数操作系统的本机复制命令一致,但也意味着该行为依赖于复制操作之前的文件系统状态。因此,复制单个文件时更可靠的做法是显式指定目标文件名,除非保证目标是将已存在的目录。

作为一种便利,如果目标包含路径(相对或绝对),则在仅复制单个源文件时,CMake 将尝试根据需要创建目标路径。这意味着在复制单个文件时,复制命令不需要早期步骤来确保目标目录存在。如果列出了多个源文件,则目标必须引用现有目录。

再次强调,CMake 的命令模式可用于确保此操作使用 make_directory 来创建命名目录,如果尚不存在,则创建命名目录,根据需要包括任何父目录。以下示例显示了如何安全地组合这些命令模式命令:

add_custom_target(CopyOne
 COMMAND ${CMAKE_COMMAND} -E copy a.txt output/textfiles/a.txt
)
add_custom_target(CopyTwo
 COMMAND ${CMAKE_COMMAND} -E make_directory output/textfiles
 COMMAND ${CMAKE_COMMAND} -E copy a.txt b.txt output/textfiles
)

copy 命令将始终将源复制到目标,即使目标已经与源相同。这导致目标时间戳始终被更新,这有时可能不太理想。如果文件已匹配,则不应更新时间戳,则 copy_if_different 命令可能更合适:

cmake -E copy_if_different file1 [file2...] destination

该命令的功能与 copy 命令完全相同,只是如果源文件已经存在于目标位置且与源文件相同,则不执行复制操作,并且目标的时间戳保持不变。还可以复制整个目录,而不是单个文件:

cmake -E copy_directory dir1 [dir2...] destination

与文件相关的复制命令不同,如果需要,目标目录将被创建,包括任何中间路径。还要注意,copy_directory 会将源目录的内容复制到目标目录中,而不是源目录本身。例如,假设目录 myDir 包含文件 someFile.txt,并且发出了以下命令:

cmake -E copy_directory myDir targetDir

结果是 targetDir 将包含文件 someFile.txt,而不是 myDir/someFile.txt。

一般来说,configure_file() 和 file() 最适合在配置时复制文件,而 CMake 的命令模式是在构建时进行复制的首选方式。虽然可以将命令模式与 execute_process() 结合使用在配置时复制文件,但没有太多理由这样做,因为 configure_file() 和 file() 都更直接,并且具有自动在任何错误停止的附加优势。

20.3. 直接读写文件

CMake 提供的不仅仅是复制文件的能力,它还提供了许多用于读取和写入文件内容的命令。file() 命令提供了大部分的功能,其中最简单的形式直接写入文件:

file(WRITE fileName content)
file(APPEND fileName content)

这两个命令都会将content写入到指定的文件中。它们之间的唯一区别在于,如果 fileName 已经存在,APPEND 将追加到现有内容,而 WRITE 在写入前会丢弃现有的内容。内容就像任何其他函数参数一样,可以是变量或字符串的内容。

例如:

set(msg "Hello world")
file(WRITE hello.txt ${msg})
file(APPEND hello.txt " from CMake")

请注意,不会自动添加换行符,因此 APPEND 行的文本将直接在 WRITE 行的文本之后继续,没有换行。要写入换行符,必须在传递给 file() 命令的内容中包含它。一种方法是使用跨多行的引用值:

file(WRITE multi.txt "First line
Second line
")

如果使用的是 CMake 3.0 或更高版本,则在第 5.1 节“变量基础”中引入的方括号语法有时可能更方便,因为它防止了内容的任何变量替换。

file(WRITE multi.txt [[
First line
Second line
]])
file(WRITE userCheck.sh [=[
#!/bin/bash
[[ -n "${USER}" ]] && echo "Have USER"
]=])

在上述示例中,要写入 multi.txt 的内容仅包含简单文本,没有特殊字符,因此最简单的方括号语法,其中可以省略 = 字符,就足够了,只留下一对方括号标记内容的开始和结束。请注意,在打开方括号后立即忽略第一个换行符的行为使命令更易读。

userCheck.sh 的内容要复杂得多,并突显了方括号语法的特点。如果没有方括号语法,CMake 将看到 ${USER} 部分并将其视为 CMake 变量替换,但由于方括号语法不执行此类替换,因此它保持不变。出于同样的原因,内容中的各种引号字符也不被解释为除内容外的任何其他内容。它们不需要被转义以防止它们被解释为参数的开始或结束。此外,请注意,嵌入内容包含一对方括号。这是变量数量的 = 符号在开始和结束标记中的用途,允许选择标记,使其不与其周围的内容匹配。当要将多行写入文件并且不应执行任何替换时,方括号语法通常是指定要写入的内容的最方便的方法。

有时,项目可能需要编写文件,其内容取决于构建类型。一个天真的方法是假设 CMAKE_BUILD_TYPE 变量可以用作替换,但这对于像 Xcode、Visual Studio 或 Ninja Multi-Config 这样的多配置生成器不起作用。相反,可以使用 file(GENERATE…) 命令:

file(GENERATE
 OUTPUT outFile
 INPUT inFile | CONTENT content
 [CONDITION expression]
 # Requires CMake 3.19 or later:
 [TARGET target]
 # Requires CMake 3.20 or later:
 [NO_SOURCE_PERMISSIONS | USE_SOURCE_PERMISSIONS |
 [FILE_PERMISSIONS permissions...]
)

这类似于 file(WRITE…),不同之处在于它为当前 CMake 生成器支持的每个构建类型编写一个文件。INPUT 或 CONTENT 选项中的任何一个必须存在,但不能同时存在。它们定义要写入指定输出文件的内容。

outFile、inFile 和 content 都支持生成器表达式,这是文件名和内容为每个构建类型自定义的方式。可以使用 CONDITION 选项跳过构建类型。在展开任何生成器表达式后,表达式必须评估为 0 或 1。如果它评估为 0,则不会生成输出文件。

如果参数中的任何生成器表达式需要一个目标才能对其进行评估,但目标不是表达式的一部分(例如 $<TARGET_PROPERTY:propName>),则必须提供 TARGET 选项以便解析它。TARGET 选项仅在使用 CMake 3.19 或更高版本时受支持。

使用 CMake 3.20 或更高版本,可以像 configure_file() 或 file(COPY) 一样指定 outFile 的权限。NO_SOURCE_PERMISSIONS 和 USE_SOURCE_PERMISSIONS 关键字仅在使用 INPUT 选项指定输入文件时有效,但 FILE_PERMISSIONS 可以与 INPUT 或 CONTENT 一起使用。

以下示例展示了如何利用生成器表达式根据构建类型自定义内容和文件名。

# Generate unique files for all but Release
file(GENERATE
 OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/outFile-$<CONFIG>.txt
 INPUT ${CMAKE_CURRENT_SOURCE_DIR}/input.txt.in
 CONDITION $<NOT:$<CONFIG:Release>>
)
# Embedded content, bracket syntax does not prevent the use of generator expressions
file(GENERATE
 OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/details-$<CONFIG>.txt
 CONTENT [[
Built as "$<CONFIG>" for platform "$<PLATFORM_ID>".
Defines: $<TARGET_PROPERTY:COMPILE_DEFINITIONS>
]]
 TARGET SomeTarget
)

在上述第一个示例中,当编写输出文件时,input.txt.in 文件中的任何生成器表达式都将被评估。这在某种程度上类似于 configure_file() 替换 CMake 变量的方式,只是这次替换是针对生成器表达式的。第二个示例演示了即使涉及生成器表达式和引号,方括号语法也可以是定义文件内容的一种特别方便的方式。

通常,每种构建类型的输出文件都会不同。然而,在某些情况下,希望输出文件始终相同,例如,文件内容不取决于构建类型,而是取决于其他一些生成器表达式。为了支持这种用例,CMake 允许对不同构建类型使用相同的输出文件,但前提是对于这些构建类型,生成的文件内容也必须相同。CMake 不允许尝试生成相同输出文件的多个 file(GENERATE…) 命令。

与 file(COPY…) 类似,file(GENERATE…) 命令仅在内容实际更改时才会修改输出文件。因此,仅当内容不同时,输出文件的时间戳才会更新。当生成的文件用作构建目标的输入时(例如生成的头文件),这非常有用,因为它可以防止不必要的重建。

与大多数其他 CMake 命令相比,file(GENERATE…) 的行为存在一些重要差异。因为它评估生成器表达式,所以无法立即写出文件。相反,文件将作为生成阶段的一部分写入,这发生在所有 CMakeLists.txt 文件都已经处理完毕之后。这意味着当 file(GENERATE…) 命令返回时,生成的文件尚不存在,因此在配置阶段无法将文件用作其他东西的输入。特别是,由于生成的文件直到配置阶段结束才存在,因此它们不能通过 configure_file()、file(COPY…) 等进行复制或读取。但是,它们仍然可以用作构建阶段的输入,例如生成的源文件或头文件。

另一个需要注意的是,在 CMake 3.10 之前,file(GENERATE…) 处理相对路径的方式与通常的 CMake 约定不同。相对路径的行为没有明确说明,通常最终会相对于调用 cmake 时的工作目录。这是不可靠和不一致的,因此在 CMake 3.10 中,将行为更改为使 INPUT 相对于当前源目录,OUTPUT 相对于当前二进制目录,就像大多数其他处理路径的 CMake 命令一样。除非将最低 CMake 版本设置为 3.10 或更高版本,否则项目应考虑相对路径不安全用于 file(GENERATE…)。

file() 命令不仅可以复制或创建文件,还可以用于读取文件的内容:

file(READ fileName outVar [OFFSET offset] [LIMIT byteCount] [HEX])

在没有任何可选关键字的情况下,该命令读取 fileName 的所有内容,并将其作为单个字符串存储在 outVar 中。OFFSET 选项可用于从指定的偏移量开始读取,该偏移量从文件开头计算的字节数。可以使用 LIMIT 选项限制要读取的字节数。如果给出 HEX 选项,则内容将转换为十六进制表示,这对于包含二进制数据而不是文本的文件可能很有用。

如果更希望按行分解文件内容,则 STRINGS 子命令可能更方便。它不会将整个文件内容作为单个字符串存储,而是将每行作为一个列表项存储。

file(STRINGS fileName outVar
 [LENGTH_MAXIMUM maxBytesPerLine]
 [LENGTH_MINIMUM minBytesPerLine]
 [LIMIT_INPUT maxReadBytes]
 [LIMIT_OUTPUT maxStoredBytes]
 [LIMIT_COUNT maxStoredLines]
 [REGEX regex]
 # ... other less commonly used options not shown
)

LENGTH_MAXIMUM 和 LENGTH_MINIMUM 选项可用于排除长度超过或少于一定字节数的字符串。可以使用 LIMIT_INPUT 限制读取的总字节数,而使用 LIMIT_OUTPUT 可以限制存储的总字节数。然而,也许更有用的是 LIMIT_COUNT 选项,它限制存储的总行数而不是字节数。

REGEX 选项是从文件中提取特定行的有用方式。例如,以下代码获取 myStory.txt 文件中包含 PKG_VERSION 或 MODULE_VERSION 的所有行的列表。

file(STRINGS myStory.txt versionLines
 REGEX "(PKG|MODULE)_VERSION"
)

它还可以与 LIMIT_COUNT 结合使用,仅获取第一个匹配项。下面的示例展示了如何结合使用 file() 和 string() 从匹配正则表达式的第一行中提取部分内容。

set(regex "^ *FOO_VERSION *= *([^ ]+) *$")
file(STRINGS config.txt fooVersion
 REGEX "${regex}"
)
string(REGEX REPLACE "${regex}" "\\1" fooVersion "${fooVersion}")

如果 config.txt 包含这样一行:

FOO_VERSION = 2.3.5

那么 fooVersion 中存储的值将是 2.3.5。

20.4. 文件系统操作

除了读写文件之外,CMake 还支持其他常见的文件系统操作。

file(MAKE_DIRECTORY dirs...)
file(REMOVE files...)
file(REMOVE_RECURSE filesOrDirs...)
file(RENAME source destination
 # CMake 3.21 or later required for these options
 [RESULT outVar]
 [NO_REPLACE]
)

MAKE_DIRECTORY 子命令将确保所列出的目录存在。必要时会创建中间路径,如果目录已经存在,则不会报错。

REMOVE 子命令可用于删除文件。如果所列出的任何文件不存在,则 file() 命令不会报错。试图使用 REMOVE 子命令删除目录将不会生效。要删除目录及其所有内容,请改用 REMOVE_RECURSE 子命令。

RENAME 子命令重命名文件或目录。源和目标必须是相同类型,即均为文件或均为目录。不允许将文件指定为源,将现有目录指定为目标。要将文件移动到目录中,必须在目标的路径部分中指定文件名。此外,目标的任何路径部分都必须已经存在 —— RENAME 形式不会创建中间目录。

通常情况下,file(RENAME) 遇到任何错误都会停止执行。从 CMake 3.21 开始,如果提供了 RESULT 关键字,则命令的结果将存储在指定的变量中。这样可以使处理过程继续执行并从错误中恢复。在成功时,存储在 outVar 中的值将为 0,否则将为错误消息。请注意,如果目标已经存在,则不会被视为错误,除非指定了 NO_REPLACE(需要 CMake 3.21 或更高版本)。没有 NO_REPLACE,则目标将被源文件静默替换。

# Requires CMake 3.21 or later
file(RENAME someFile toSomethingElse
 RESULT result
 NO_REPLACE
)

if(result)
 message(WARNING "File rename failed, taking action")
 # ... handle failure to rename the file
endif()

CMake 的命令模式也支持一套非常相似的功能集,可以在构建时使用,而不是在配置时使用:

cmake -E make_directory dirs...
cmake -E remove [-f] files... # deprecated from CMake 3.17
cmake -E remove_directory dir # deprecated from CMake 3.17
cmake -E rm [-rRf] filesOrDirs...
cmake -E rename source destination

这些命令在行为上与基于 file() 的命令基本相同,只有细微差异。remove_directory 命令严格来说只能与单个目录一起使用,而 file(REMOVE_RECURSE…) 可以删除多个项,文件和目录都可以列出。remove 命令接受一个可选的 -f 标志,该标志用于在尝试删除不存在的文件时更改行为。文档中关于此标志的行为是,没有 -f,则返回非零退出码,而有 -f,则返回零退出码。这意味着它意在模拟 Unix 的 rm -f 命令的某些行为。

不幸的是,由于实现中长期存在的 bug,这并不是实际的行为,无论是否使用 -f 标志,cmake -E remove 的退出码都应被视为不可靠。rm 命令在 CMake 3.17 中添加,作为 remove 和 remove_directory 命令的替代品。它修复了退出码错误,并且更接近于 Unix 的 rm 命令的行为。

CMake 3.14 添加了两个新的 file() 子命令,使项目能够查询和操作文件系统链接:

file(READ_SYMLINK linkName outVar)
file(CREATE_LINK pointedTo linkName
 [RESULT outVar]
 [COPY_ON_ERROR]
 [SYMBOLIC]
)

READ_SYMLINK 子命令给出 linkName 指向的路径。请注意,符号链接通常使用相对路径,对于这种情况,存储在 outVar 中的值只是原始的相对路径。

CREATE_LINK 命令允许项目创建硬链接或符号链接。默认情况下会创建硬链接,但是可以通过给出 SYMBOLIC 选项来创建符号链接。在大多数情况下,建议使用 SYMBOLIC,因为它支持更多的情景(例如,在不同的文件系统之间进行链接)。可以使用 RESULT 关键字来命名变量,以存储操作的结果。成功时存储的值为 0,否则为错误消息。如果没有 RESULT 选项,失败将导致 CMake 停止执行,并显示致命错误。COPY_ON_ERROR 选项为创建链接失败提供了备用方案,将操作降级为将 pointedTo 复制到 linkName。它主要存在是为了允许在不支持创建链接的情况下使用该命令,例如在不同驱动器或设备上创建到路径的硬链接。

所有版本的 CMake 都允许使用 CMake 的命令模式基本创建符号链接(不能通过此方法创建硬链接):

cmake -E create_symlink pointedTo linkName

CMake 3.14 还添加了查询文件大小的功能:

file(SIZE fileName outVar)

指定的 fileName 必须存在,并且非常重要的是它也必须是可读的。

CMake 3.19 添加了用于设置文件和目录权限的 file() 子命令:

file(CHMOD | CHMOD_RECURSE
 files... directories...
 [PERMISSIONS permissions...]
 [FILE_PERMISSIONS permissions...]
 [DIRECTORY_PERMISSIONS permissions...]
)

CHMOD 和 CHMOD_RECURSE 子命令在行为上相同,不同之处在于后者还将递归进入子目录。权限支持的值与 file(COPY) 子命令支持的值相同。FILE_PERMISSIONS 或 DIRECTORY_PERMISSIONS 仅适用于其各自类型的实体,并且它们将覆盖该实体类型的 PERMISSIONS。可以仅指定两种更具体的类型之一,以仅对该类型的实体进行操作。以下显示了如何利用这一点,只为目录设置权限,而不修改文件权限:

file(CHMOD_RECURSE ${someFilesAndDirs}
 DIRECTORY_PERMISSIONS
 OWNER_READ OWNER_WRITE OWNER_EXECUTE
 GROUP_READ GROUP_EXECUTE
 WORLD_READ WORLD_EXECUTE
)

20.5. 文件通配

CMake 还支持使用递归或非递归形式的通配符列出一个或多个目录的内容:

file(GLOB outVar
 [LIST_DIRECTORIES true|false]
 [RELATIVE path]
 [CONFIGURE_DEPENDS] # Requires CMake 3.12 or later 
 expressions...
)

file(GLOB_RECURSE outVar
 [LIST_DIRECTORIES true|false]
 [RELATIVE path]
 [FOLLOW_SYMLINKS]
 [CONFIGURE_DEPENDS] # Requires CMake 3.12 or later
 expressions...
)

这些命令会查找所有文件的名称与提供的表达式之一匹配的文件,可以将其视为简化的正则表达式。可以更容易地将它们视为普通的通配符,并附加字符子集选择。对于 GLOB_RECURSE,它们还可以包括路径组件。 一些示例应该有助于澄清基本用法:

对于 GLOB,匹配表达式的文件和目录都存储在 outVar 中。另一方面,对于 GLOB_RECURSE,默认情况下不包括目录名称,但可以使用 LIST_DIRECTORIES 选项进行控制。此外,对于 GLOB_RECURSE,默认情况下,指向目录的符号链接通常被报告为 outVar 中的条目,而不是进入其中,但 FOLLOW_SYMLINKS 选项指示 CMake 进入目录而不是列出它。

返回的文件名集合默认为完整的绝对路径,而不管使用的表达式如何。可以使用 RELATIVE 选项将此行为更改为报告的路径相对于特定目录。

set(base /usr/share)
file(GLOB_RECURSE images
 RELATIVE ${base}
 ${base}/*/*.png
)

上述将查找 /usr/share 下的所有图像,并包含这些图像的路径,但剥离了 /usr/share 部分。请注意,表达式中的 /*/ 允许匹配基本点下的任何目录。 开发人员应该注意,file(GLOB…) 命令不像 Unix find shell 命令那样快速。因此,如果使用它来搜索包含许多文件的文件系统的部分,则运行时间可能会很长。

file(GLOB) 和 file(GLOB_RECURSE) 命令是 CMake 中最常被误用的部分之一。它们不应用于收集一组源文件、头文件或任何作为构建输入的其他类型的文件。应该避免这样做的一个原因是,如果添加或删除了文件,CMake 不会自动重新运行,因此构建不会意识到变化。如果开发人员正在使用版本控制系统并在分支之间切换等情况下,文件集可能会发生变化,但不会导致 CMake 重新运行。CMake 3.12 中添加的 CONFIGURE_DEPENDS 选项试图解决此缺陷,但会带来性能损失,并不保证所有项目生成器都支持。应该避免使用此选项。

不幸的是,很常见的是看到教程和示例使用 file(GLOB) 和 file(GLOB_RECURSE) 来收集要传递给 add_executable() 和 add_library() 等命令的源文件集。CMake 文档明确反对这样的做法。这样的做法也忽略了一些文件可能仅适用于特定平台的可能性。对于分布在多个目录中的许多文件的项目,有更好的方法来收集源文件集,这些方法不会受到这些缺陷的影响。第 34.5.1 节,“跨目录构建目标”提供了避免这些问题并鼓励更模块化、自包含的目录结构的替代策略。

20.6. 下载和上传

file() 命令有许多其他形式,执行不同的任务。一个令人惊讶的强大子命令对项目具有从任意 URL 下载文件和上传文件的能力。

file(DOWNLOAD url fileName [options...])
file(UPLOAD fileName url [options...])

DOWNLOAD 形式从指定的 URL 下载文件并将其保存到 fileName 中。如果给出相对 fileName,则解释为相对于当前二进制目录。CMake 3.19 及更高版本允许省略 fileName,此时文件将被下载但被丢弃。这可用于检查 URL 是否存在,而无需将文件保存在任何地方(不建议对预期为大文件的文件使用)。

UPLOAD 形式执行相反的操作,将指定的文件上传到指定的 URL。对于上传,相对路径被解释为相对于当前源目录。

可以使用以下选项,其中大部分适用于 DOWNLOAD 和 UPLOAD:

LOG outVar

将操作的记录输出保存到指定的变量中。当下载或上传失败时,这可能有助于诊断问题。

SHOW_PROGRESS

当存在时,此选项会导致将进度信息记录为状态消息。这可能会导致 CMake 配置阶段非常嘈杂,因此最好仅在临时帮助测试连接失败时使用此选项。

TIMEOUT seconds

如果超过秒数,则中止操作。

INACTIVITY_TIMEOUT seconds 这是一种更具体的超时类型。某些网络连接可能质量较差或速度很慢。可能希望允许操作继续进行,只要它正在取得某种进展,但如果超过某个可接受的限制时间停顿,操作应该失败。INACTIVITY_TIMEOUT 选项提供了此功能,而 TIMEOUT 只允许总时间受限。

TLS_VERIFY value

此选项接受一个布尔值,指示在从 https:// URL 下载或上传时是否执行服务器证书验证。如果未提供此选项,CMake 将查找名为 CMAKE_TLS_VERIFY 的变量。如果未定义选项或变量,则默认行为是不验证服务器证书。请注意,上传对此选项的支持仅在 CMake 3.18 中添加。

TLS_CAINFO fileName

可以使用此选项指定自定义证书颁发机构文件。它仅影响 https:// URL。如果未提供此选项,CMake 将查找名为 CMAKE_TLS_CAINFO 的变量。请注意,上传对此选项的支持仅在 CMake 3.18 中添加。

EXPECTED_HASH ALGO=value

此选项仅支持 DOWNLOAD。它指定要下载的文件的校验和,以便 CMake 可以验证内容。ALGO 可以是 CMake 支持的任何散列算法,最常用的是 MD5 和 SHA1。一些较旧的项目可能使用 EXPECTED_MD5 作为 EXPECTED_HASH MD5=… 的替代形式,但新项目应优先选择 EXPECTED_HASH 形式。

从 CMake 3.7 开始,以下选项也适用于 DOWNLOAD 和 UPLOAD:

USERPWD username:password

为操作提供身份验证详细信息。请注意,硬编码密码是一个安全问题,通常应避免。如果使用此选项提供密码,则内容应来自于项目外部,例如从用户的本地机器上的配置时间读取的适当受保护的文件。

HTTPHEADER header

为操作包含 HTTP 标头,如有需要可以重复多次以提供多个标头值。以下部分示例演示了此选项的一个动机案例之一:

file(DOWNLOAD "https://somebucket.s3.amazonaws.com/myfile.tar.gz" myfile.tar.gz
 EXPECTED_HASH SHA1=${myfileHash}
 HTTPHEADER "Host: somebucket.s3.amazonaws.com"
 HTTPHEADER "Date: ${timestamp}"
 HTTPHEADER "Content-Type: application/x-compressed-tar"
 HTTPHEADER "Authorization: AWS ${s3key}:${signature}"
)

CMake 3.24 添加了仅下载文件的一部分的能力:

RANGE_START offset

RANGE_END offset

这些偏移量分别是文件开始处的字节数。省略 RANGE_START 将从文件的开头开始下载。省略 RANGE_END 将下载到文件的末尾。

基于 file() 的下载和上传命令通常更多地用作安装步骤、打包或测试报告的一部分,但它们偶尔也可用于其他用途。示例包括在配置时间下载引导文件或将不应存储为项目源代码的一部分的文件引入构建中(例如,应仅对某些开发人员可访问的敏感文件、非常大的文件等)。后续章节提供了这些命令被用于极好效果的特定场景。

20.7. 推荐做法

本章介绍了与文件处理相关的一系列 CMake 功能。这些各种方法可以非常有效地以一种与平台无关的方式执行各种任务,但它们也可能被误用。建立良好的模式并在整个项目中一致地应用它们将有助于确保新开发人员接触到更好的实践方法。如果项目的最低 CMake 版本为 3.20 或更高,则考虑使用 cmake_path() 命令进行路径处理。它通常比更老的 get_filename_component() 和 file() 命令的类似功能具有更一致的语法。另一方面,cmake_path() 不访问底层文件系统,因此如果需要解析符号链接,则无法使用它。请注意,if(IS_ABSOLUTE) 和 cmake_path(IS_ABSOLUTE) 命令基于主机平台解释路径,但两者之间存在微妙的差异。它们在某些情况下可能针对相同路径产生不同的结果。cmake_path(IS_ABSOLUTE) 遵循 C++ std::filesystem::path::is_absolute() 函数的实现,而 if(IS_ABSOLUTE) 使用自己的逻辑处理某些特殊情况。如果从 Windows 主机交叉编译到非 Windows 目标平台,或反之,则需要格外小心。如果任一命令被不当使用,一些路径可能会产生未定义的行为,或者给出与直觉预期相反的结果。

configure_file() 命令是新开发人员经常忽视的一个命令,但它是提供文件的关键方法之一,其内容可以根据配置时间确定的变量进行定制,甚至只是简单的文件复制。常见的命名约定是源和目标文件名部分相同,除了源文件附加了额外的 .in 后缀。一些 IDE 环境理解此约定,并将基于文件扩展名而不带 .in 后缀的源文件提供适当的语法高亮显示。.in 后缀的存在不仅作为文件需要在使用之前进行转换的明确提醒,还可以防止在 CMake 或编译器查找多个目录中意外拾取到它而不是目标。这在目标是 C/C++ 头文件且当前源和二进制目录都在头文件搜索路径上时尤为重要。

选择最适合复制文件的命令并不总是清晰的。在选择 configure_file()、file(COPY) 和 file(INSTALL) 之间可能会提供一个有用的指导:

  • 如果需要修改文件内容以包含 CMake 变量替换,则 configure_file() 是实现的最简洁方式。
  • 如果只需复制文件但其名称将更改,则 configure_file() 的语法稍微比 file(COPY…) 短,但两者都是合适的。
  • 如果要复制多个文件或整个目录结构,则必须使用 file(COPY) 或 file(INSTALL) 命令。
  • 如果需要对复制的文件或目录权限进行控制,则如果项目的最低 CMake 版本为 3.19 或更早,则必须使用 file(COPY) 或 file(INSTALL)。
  • file(INSTALL) 通常仅应作为安装脚本的一部分使用。其他情况下应优先选择 file(COPY)。

在 CMake 3.10 之前,file(GENERATE…) 命令对相对路径的处理与 CMake 提供的大多数其他命令不同。项目应始终优先指定 INPUT 和 OUTPUT 文件的绝对路径,以避免错误或在意外位置生成文件,而不是依赖开发人员了解此不同行为。

使用 file(DOWNLOAD…) 或 file(UPLOAD…) 命令下载或上传文件时,应仔细考虑安全性和效率方面的问题。努力避免将任何形式的认证详细信息(用户名、密码、私钥等)嵌入到存储在版本控制系统中的项目源代码的任何文件中。此类详细信息应来自项目外部,例如通过环境变量(仍然有些不安全)、在用户文件系统上找到具有适当权限限制访问的文件,或某种密钥链。在下载时利用 EXPECTED_HASH 选项重复使用先前下载内容,避免潜在耗时的远程操作。如果无法事先知道下载文件的哈希值,则强烈建议使用 TLS_VERIFY 选项确保内容的完整性。还可以考虑指定 TIMEOUT、INACTIVITY_TIMEOUT 或两者,以防止配置运行在网络连接质量差或不可靠时无限期阻塞。