第18章:文件处理

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

  • 构造路径或提取组件的路径。

  • 从目录中获取文件列表。

  • 复制文件。

  • 使用字符串内容生成一个文件。

  • 使用另一个文件的内容生成一个文件。

  • 读取文件的内容。

  • 计算文件的校验和或哈希值。

CMake提供各种相关处理文件和目录的特性。某些情况下,实现同一件事可能有多种方式,因此了解不同的选择并了解如何有效地使用它们非常重要。这些特性中有许多经常误用,有些是由于在线教程和示例中普遍存在的误用,导致人们相信这是正确的做法。本章将讨论一些有问题的反模式。

CMake的大部分与文件相关的功能是由file()命令提供,其他一些命令提供了更适合的替代方案,或者提供了相关的帮助功能。CMake的命令模式(在前一章中介绍过)也提供了各种与文件相关的特性,这些特性与file()所提供的特性有很大的重叠,但它只涵盖了file()的一些场景,而不是大多数情况下的替代方案。

18.1. 配置路径

文件处理的最基本部分之一是处理文件名和路径。项目通常需要从完整路径中提取文件名、文件后缀等,或者在绝对路径和相对路径之间进行转换。执行这些操作的主要方法是get_filename_component()命令,该命令有三种不同的形式。第一种形式允许提取路径或文件名的不同部分:

get_filename_component(outVar input component [CACHE])

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

DIRECTORY

提取输入的路径部分,但不包含文件名。CMake 2.8.12之前,这个选项是PATH,为了保持与旧版本的兼容性,它仍然接受为DIRECTORY的同义词。

NAME

提取完整的文件名,包括扩展名。这实际上只是丢弃了输入的目录部分。

NAME_WE

只提取基本文件名。这是类似的名称,除了只有文件名的一部分,但不包括提取的第一个“.”。

EXT

这是NAME_WE的补充。它从第一个"."开始提取文件名的扩展部分。

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(baseName ${input} NAME_WE) # foo
get_filename_component(extension ${input} EXT) # .bar.txt

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

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

这种形式下,输入可以是相对路径,也可以是绝对路径。如果给定了BASE_DIR,相对路径将解释为相对于dir而不是当前源目录(即CMAKE_CURRENT_SOURCE_DIR)。如果输入已经是绝对路径,则忽略BASE_DIR。

组件决定如何处理符号链接时,会将计算路径存储在outVar:

ABSOLUTE

计算输入的绝对路径而不需要解析符号链接。

REALPATH

解析符号链接后,计算输入的绝对路径。

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

file(RELATIVE_PATH outVar relativeToDir input)

下面的例子演示了它的用法:

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

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

# The variables now have the following values:
# fooBar = foo/bar
# other = ../other/place

get_filename_component()命令的第三种形式可以方便地提取完整命令行的部分内容:

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

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

CMake在所有文件处理中,大多数时候一个项目可以在所有平台上使用正向斜杠作为目录分隔符,CMake在项目需要时转换为本地路径。然而,项目有时可能需要显式地在CMake和本机路径之间进行转换,比如:使用自定义命令时,需要向需要本机路径的脚本传递路径。对于这些情况,file()命令提供了另外两种形式,帮助转换平台原生和CMake之间的路径:

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

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

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

18.2. 复制文件

配置阶段或构建过程中,复制文件是一种比较常见的需求。因为复制文件对大多数用户来说是一项熟悉的任务,所以对于CMake的新开发人员来说,用他们已知的方法来实现文件复制是很自然的。不幸的是,这通常会导致使用特定于平台的shell命令(add_custom_target()和add_custom_command()),有时还会出现依赖问题,需要开发人员多次运行CMake和/或按特定顺序手动构建目标。不过,CMake提供了更好的替代方法。

本节中,将介绍一些复制文件的方法。有些是为了满足特定的需要,而另一些则是为了更加通用,可以在各种情况下使用。所有方法在所有平台上的工作方式完全相同。

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

configure_file(source destination [COPYONLY | @ONLY] [ESCAPE_QUOTES])

源文件必须是一个已经存在的文件,可以是绝对路径,也可以是相对路径,后者相对于当前的源目录(即CMAKE_CURRENT_SOURCE_DIR)。目标不能简单地将文件复制到一个目录中,它必须是文件名,可选路径可以是绝对路径或相对路径。如果目标不是绝对路径,可以相对于当前二进制目录(即CMAKE_CURRENT_BINARY_DIR)。如果目标路径的任何部分不存在,CMake将尝试创建丢失的目录。注意,经常会看到项目将CMAKE_CURRENT_SOURCE_DIR或CMAKE_CURRENT_BINARY_DIR分别作为源和目标路径的一部分,但这只会增加不必要的混乱,应该避免。

如果修改源文件,构建将认为目标文件已经过期,并将自动重新运行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变量的一种有效且常见的方法。在这种情况下,替换可以仅限于使用@ONLY关键字的@someVar@形式:

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()命令也可以提供相同的功能:

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

这些选项的含义与configure_file()相同。如果要复制的内容需要更复杂的步骤,而不仅仅是简单的替换(下一节将给出一个替换示例),则此方式很有用。

如果不需要替换,另一种选择是使用file()命令复制或安装,二者都支持相同的选项:

file(<COPY|INSTALL> fileOrDir1 [fileOrDir2...]
 DESTINATION dir
 [NO_SOURCE_PERMISSIONS | USE_SOURCE_PERMISSIONS |
 [FILE_PERMISSIONS permissions...]
 [DIRECTORY_PERMISSIONS permissions...]]
 [FILES_MATCHING]
 [PATTERN pattern | REGEX regex] [EXCLUDE]
 [PERMISSIONS permissions...]
 [...]
)

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

如果源文件是一个目录名,将复制到目标文件中。要将目录中的内容复制到目标目录中,需要在源目录中附加一个正斜杠(/),如下所示:

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_WRITE
)

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

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为复制文件和目录提供了第三种选择。尽管configure_file()和file()都在配置时使用,或者在安装时作为CMake脚本的一部分,但CMake的命令模式也可以用在构建时复制文件和目录。命令模式是作为add_custom_target()和add_custom_command()规则的一部分,是复制的首选方式,因为它提供了平台独立性命令。有三个与复制相关的命令,第一个用于复制单个文件:

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

如果只提供了单个源文件,那么destination会解释为要复制到的文件的名称,除非指定了一个存在目录。当目标是一个存在目录时,源文件将会复制到其中。这种行为与大多数操作系统的复制命令是一致的,但这也意味着这种行为依赖于复制操作前文件系统的状态。由于这个原因,复制单个文件时始终显式地指定目标文件名更健壮,除非保证目标是一个存在的目录。

如果目的地包括路径(相对或绝对),CMake将尝试创建目的地路径,和复制单个源文件一样。这意味着,复制单个文件时,copy命令不需要前面的步骤来确保目标目录存在。但是,如果列出了多个源文件,destination必须引用存在的目录。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()都更直接,而且还具有在出现错误时自动停止的优点。

18.3. 直接读写文件

CMake提供的不仅仅是复制文件的能力,还提供了许多用于读写文件内容的命令。file()命令提供了大量的功能,最简单的是直接写入文件:

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

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

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

上面的示例将生成hello.txt文件,其中包含一行来自CMake的文本Hello world。注意,新行不会自动添加,因此上面示例中的APPEND行中的文本直接继续在WRITE行的文本之后,而不会中断。要写入换行符,它必须包含在传递给file()命令的内容中。一种方法是使用跨多行引用的值:

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

如果使用CMake 3.0或更高版本,使用lua括号语法有时会更方便,因为可以防止对内容进行任何变量替换。

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这样的多配置生成器不起作用。所以,可以使用file(GENERATE…)命令:

file(GENERATE
 OUTPUT outFile
 INPUT inFile | CONTENT content
 [CONDITION expression]
)

工作原理有点像file(WRITE…),除了会为当前CMake生成器支持的每种构建类型写入一个文件。INPUT或CONTENT选项必须出现,但不能同时出现。它们定义了要写入指定输出文件的内容。所有的参数都支持生成器表达式,这就可以为每种构建类型定制文件名和内容。可以使用CONDITION选项跳过构建类型,并使用一个表达式,对于要跳过的构建类型,表达式的计算值为0,对于要生成的构建类型,表达式的计算值为1。

下面的示例,说明如何使用生成器表达式根据构建类型自定义内容和文件名。

# 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>".
]])

上面的第一种情况中,input.txt内容中的任何生成器表达式。将在写入输出文件时计算。这有点类似于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中,行为就改变了,使输入路径作为相对于当前源目录,输出路径作为相对于当前二进制目录,就像大多数其他路径处理的CMake命令一样。除非最小CMake版本设置为3.10或更高版本,否则项目应该考虑使用file(GENERATE…)时使用的相对路径靠不靠谱。

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

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

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

如果希望逐行分解文件内容,则字符串的形式可能更方便。这个方式不是将整个文件的内容存储为单个字符串,而是将其存储为一个列表,每一行都是一个列表项。以下简化的显示了常用的选项:

file(STRINGS fileName outVar
 [LENGTH_MAXIMUM maxBytesPerLine]
 [LENGTH_MINIMUM minBytesPerLine]
 [LIMIT_INPUT maxReadBytes]
 [LIMIT_OUTPUT maxStoredBytes]
 [LIMIT_COUNT maxStoredLines]
 [REGEX regex]
)

上面没有显示的选项与编码、特殊文件类型的转换或换行字符的处理,这些在大多数情况下都不需要。详细信息请参考CMake文档。

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。

18.4. 配置文件系统

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

file(RENAME source destination)
file(REMOVE files...)
file(REMOVE_RECURSE filesOrDirs...)
file(MAKE_DIRECTORY dirs...)

RENAME重命名文件或目录,如果目标已经存在,则以静默方式替换它。源和目标必须是相同的类型,即两个文件或两个目录。不允许指定文件和目录作为目的。若要将文件移动到目录中,必须将文件名指定为目标文件的一部分。此外,目的的任何路径必须存在,RENAME将不会创建中间目录。

REMOVE可用于删除文件。如果列出的任何文件不存在,file()命令不会报告错误。试图用REMOVE删除目录将不起作用。要删除目录及其所有内容,请使用REMOVE_RECURSE形式。

MAKE_DIRECTORY表单将确保列出的目录存在,根据需要创建中间路径,如果目录已经存在,不会报错。

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

cmake -E rename source destination
cmake -E remove [-f] files...
cmake -E remove_directory dir
cmake -E make_directory dirs...

这些命令的行为基本上与file()的对应命令相当,只有细微的变化。remove_directory命令只能用于单个目录,而file(REMOVE_RECURSE…)可以删除多个项,并且文件和目录都可以列出。remove命令接受一个可选的-f标志,当试图删除不存在的文件时,它会改变行为。如果没有-f,将返回一个非零的退出码,而使用-f,将返回一个零的退出码。这是模拟了Unix rm -f命令的行为。

CMake还支持用递归或非递归的globbing方式列出一个或多个目录的内容:

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命令快。因此,如果使用运行时搜索文件系统中包含许多文件的部分,那么运行时长是非常重要的。

file(GLOB)和file(GLOB_RECURSE)会经常误用。它们不应用于收集一组用作源文件、头文件或充当构建输入的文件集。避免这种情况的原因之一是,如果文件被添加或删除,CMake不会自动重新运行,因此构建不会意识到更改。如果开发人员使用了版本控制系统,并在分支之间进行切换,这就成问题了。在这些地方,文件集可能会发生变化,但不会导致CMake重新运行。CMake 3.12中添加的CONFIGURE_DEPENDS选项试图解决这个缺陷,但是它会带来性能损失,并且只对某些生成器有效,所以最好不要使用这个选项。

不幸的是,经常可以看到教程和示例使用file(GLOB)和file(GLOB_RECURSE)收集要传递给add_executable()和add_library()等命令的源文件集。由于上述原因,CMake文档明确不鼓励这样做。对于跨多个目录有许多文件的项目,有更好的方法来收集源文件集。第28.5.1节提出了一些备选策略,这些策略不仅避免了这些问题,而且鼓励采用更模块化和自包含的目录结构。

18.5. 下载和上传

file()命令有许多其他使用方式,它们执行不同的任务。其子命令提供了从URL下载文件和将文件上传到URL的功能。

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

DOWNLOAD从指定的url下载一个文件并将其保存为fileName。如果给出了相对文件名,它将被解释为相对于当前二进制目录。UPLOAD执行反向操作,将命名的文件上载到指定的url。对于上传,相对路径解释为相对于当前源目录。DOWNLOAD和UPLOAD都有一些常见的选项:

LOG outVar

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

SHOW_PROGRESS

该选项出现时,会将进度信息作为状态消息记入日志。这可能会产生一个相当“嘈杂”的CMake配置阶段,所以可以使用这个选项作为测试一个失败的连接的调试方法。

TIMEOUT seconds

如果经过的时间超过设置的秒数,则中止操作。

INACTIVITY_TIMEOUT seconds

更具体的超时设置。有些网络连接的质量可能很差,或者可能只是很慢。允许操作继续下去,只要它有速度,但是如果它停止的时间超过了可接受的时限,操作就会失败。INACTIVITY_TIMEOUT选项提供了这种功能,而TIMEOUT只允许限制总时间。

DOWNLOAD还支持更多的选择:

EXPECTED_HASH ALGO=value

指定正在下载的文件的校验和,以便CMake可以验证内容。ALGO可以是支持的任意一种哈希算法,最常用的是MD5和SHA1。一些较老的项目可能使用EXPECTED_MD5作为EXPECTED_HASH MD5=…的替代,但是新的项目应该使用EXPECTED_HASH。

TLS_VERIFY value

此选项接受一个布尔值,该值指示从https:// url下载时是否执行服务器证书验证。如果没有提供此选项,则CMake将查找名为CMAKE_TLS_VERIFY的变量。如果选项和变量都没有定义,默认是不验证服务器证书。

TLS_CAINFO fileName

可以使用此选项指定自定义证书文件。它只影响https:// url。

对于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}"
)

基于file()的下载和上传命令通常用作安装步骤、打包或测试报告的一部分,但偶尔也用于其他目的。例如,在配置时下载引导文件,或者将不能或不应该作为项目源的一部分存储的文件放入构建中(例如,只有特定开发人员才能访问的敏感文件,非常大的文件等等)。

18.6. 推荐

本章介绍了一系列与文件处理相关的CMake功能。可以使用各种方法有效地执行一系列的任务。建立良好的习惯和模式,并在整个项目中始终如一地应用它们,将有助于新开发人员接触到更好的实践。

configure_file()命令是新开发人员经常忽略的一个命令,它是提供一个文件的关键方法,该文件的内容可以在配置时确定并进行调整,甚至只是为了进行简单的文件复制。一个常见的命名约定是源和目标的文件名部分是相同的,除了源有一个额外的.in作为后缀。一些IDE环境理解这种约定,并且仍然会根据文件的扩展名(不带.in后缀)在源文件上提供适当的语法高亮显示。后缀不仅作为一个清晰的提醒,在文件需要转换/复制之前使用,如果CMake或编译器寻找文件在多个目录,也可以防止目标意外的使用。当目标文件是C/C++头文件时,并且当前源目录和二进制目录都在头文件搜索路径上时,这就很重要了。

为复制文件选择最合适的命令并不总是很清楚。在configure_file()、file(COPY)和file(INSTALL)之间进行选择时,以下内容可以作为指导:

  • 如果需要修改文件内容用CMake变量替换,configure_file()是合适的选择。

  • 如果只需要复制一个文件,但是文件名会发生变化,那么configure_file()的语法比file(COPY…)稍微短一些,两者都适合。

  • 如果复制多个文件或整个目录结构,使用file(COPY)或file(INSTALL)命令会更好。

  • 如果需要控制文件或目录权限作为复制的一部分,必须使用file(COPY)或file(INSTALL)。

  • file(INSTALL)通常作为安装脚本的一部分使用。在其他情况下,最好使用file(COPY)。

CMake 3.10之前,与CMake提供的大多数其他命令相比,file(GENERATE…)命令对相对路径的处理是不同的。项目不应该依赖于开发人员能意识到这种不同的行为,而是应该始终使用绝对路径指定输入和输出文件,以避免错误或文件在意外的位置生成。

使用file(DOWNLOAD…)或file(UPLOAD…)命令下载或上传文件时,要仔细考虑安全和效率的问题。尽量避免在项目源的版本控制系统中存储的任何文件中嵌入任何类型的认证细节(用户名、密码、私钥等)。这些细节应该来自项目外部,比如:通过环境变量(仍然有些不安全)、用户文件系统中找到的具有适当权限限制访问的文件或某种类型的密钥链。下载时使用EXPECTED_HASH选项,重用以前运行时下载的内容,避免可能耗时的远程操作。如果下载的文件的哈希值不能提前知道,那么强烈建议使用TLS_VERIFY选项来确保内容的完整性。还可以考虑指定一个TIMEOUT、INACTIVITY_TIMEOUT或两者同时指定,以防止在网络连接不佳或不可靠的情况下,配置时的无限期阻塞。

Last updated