第十九章:自定义任务

Posted by lili on

任何构建工具都不可能实现每一个项目所需的所有功能。开发人员将需要执行一些超出直接支持功能范围的任务。例如,可能需要运行特殊工具来生成源文件或在构建目标后进行后处理。可能需要复制、验证文件或计算哈希值。构建产物可能需要归档或通知服务联系。这些和其他任务并不总是符合可预测模式,无法轻松提供为通用构建系统功能。

CMake通过自定义命令和自定义目标来支持此类任务。这些允许在构建时执行任何命令或一系列命令,以执行项目需要的任意任务。CMake还支持在配置时执行任务,从而实现了在构建阶段之前或甚至在处理当前CMakeLists.txt文件的后续部分之前完成任务的各种技术。

19.1. 自定义目标

库和可执行目标并不是CMake支持的唯一目标类型。项目还可以定义自己的自定义目标,执行定义为一系列命令的任意任务,在构建时执行。使用add_custom_target()命令定义这些自定义目标:

add_custom_target(targetName
 
 [ALL]
 
 [command1 [args1...]]
 
 [COMMAND command2 [args2...]]
 
 [DEPENDS depends1...]
 
 [BYPRODUCTS [files...]]
 
 [WORKING_DIRECTORY dir]
 
 [COMMENT comment]
 
 [VERBATIM]
 
 [USES_TERMINAL]
 # Requires CMake 3.2 or later
 
 [JOB_POOL poolName] # Requires CMake 3.15 or later
 
 [SOURCES source1 [source2...]]
)

将使用指定的targetName创建一个新目标,并且它始终会被认为是过期的。ALL选项使得所有目标都依赖于这个新的自定义目标(各种生成器对所有目标的命名略有不同,但通常是类似于all、ALL或类似的)。如果没有提供ALL选项,则只有在明确请求或者构建依赖于它的其他目标时,才会构建该目标。

构建自定义目标时,指定的命令会按照给定的顺序执行,每个命令可以有任意数量的参数。为了提高可读性,参数可以跨越多行分割。第一个命令不需要在其前面加上COMMAND关键字,但为了清晰起见,建议始终包括COMMAND关键字,即使是第一个命令。当指定多个命令时,这一点尤为重要,因为它使得每个命令都使用一致的形式。

命令可以定义为在主机平台上可以执行的任何操作。典型的命令涉及运行脚本或系统提供的可执行文件,但它们也可以运行作为构建的一部分创建的可执行目标。如果另一个可执行目标名称列为要执行的命令,则CMake将自动替换该其他目标的可执行文件的构建位置。这适用于无论使用的平台或CMake生成器,从而使项目摆脱了必须解决导致一系列不同输出目录结构、文件名等的各种平台和生成器差异的困扰。如果另一个目标需要用作命令的参数,CMake将不会自动执行相同的替换,但可以通过TARGET_FILE生成器表达式轻松获取等效替换。

项目应该利用这些功能,让CMake提供目标的位置而不是手动硬编码路径,因为这样可以使项目在所有平台和生成器类型上都能够健壮支持,而且付出的努力很小。以下示例显示了如何定义一个自定义目标,该目标在命令和参数列表中使用了另外两个目标:

add_executable(Hasher hasher.cpp)
add_library(MyLib api.cpp)

add_custom_target(CreateHash
 COMMAND Hasher $<TARGET_FILE:MyLib>
)

当目标被用作要执行的命令时,CMake会自动创建对该可执行目标的依赖关系,以确保在自定义目标之前构建它。类似地,如果一个目标在命令或其参数的任何位置引用了以下生成器表达式,则也会自动创建对该目标的依赖关系:

  • $<TARGET_FILE:…>
  • $<TARGET_LINKER_FILE:…>
  • $<TARGET_SONAME_FILE:…>
  • $<TARGET_PDB_FILE:…>

对于CMake 3.18及更早版本,其他$<TARGET_xxx:…>生成器表达式也会导致自动添加依赖关系。对于CMake 3.19或更高版本,行为取决于策略CMP0112。有关更多详细信息,请参阅该策略的CMake文档。要在未提及此类生成器表达式的目标上创建依赖关系,可以使用add_dependencies()命令定义该关系。

如果依赖关系存在于文件而不是目标上,则可以使用DEPENDS关键字来指定该关系。请注意,DEPENDS不应用于目标依赖关系,仅用于文件依赖关系。当列出的文件是由其他一些自定义命令生成的(见下面的第19.3节,“生成文件的命令”),CMake将设置必要的依赖关系,以确保其他自定义命令在此自定义目标命令之前执行。始终使用绝对路径来定义DEPENDS,因为相对路径可能会因为一个允许针对多个位置进行路径匹配的传统特性而产生意外结果。

当提供多个命令时,每个命令将按照列出的顺序执行。然而,项目不应该假设任何特定的Shell行为,因为每个命令可能在自己独立的Shell中运行,或者根本不使用任何Shell环境。自定义命令应该被定义为在隔离环境中执行,并且没有任何Shell特性,如重定向、变量替换等,只有命令顺序被强制执行。虽然其中一些特性可能在某些平台上工作,但并不是普遍支持的。另外,由于没有保证任何特定的Shell行为,所以在可执行文件名称或其参数中进行转义可能在不同的平台上处理方式不同。为了减少这些差异,可以使用VERBATIM选项确保仅当解析CMakeLists.txt文件时CMake本身进行转义。平台不再执行进一步的转义,因此开发人员可以放心地确定命令最终是如何构造以执行的。如果存在转义相关的可能性,建议使用VERBATIM关键字。

命令执行的目录默认为当前二进制目录。可以使用WORKING_DIRECTORY选项更改此选项,该选项可以是绝对路径或相对路径,后者相对于当前二进制目录。这意味着使用${CMAKE_CURRENT_BINARY_DIR}作为工作目录的一部分应该是不必要的,因为相对路径已经暗示了它。

BYPRODUCTS选项可用于列出作为运行命令集的副产品而创建的其他文件。如果正在使用Ninja生成器,并且其他目标依赖于运行此自定义命令集时生成的任何文件,则需要此选项。BYPRODUCTS中列出的文件被标记为GENERATED(对于所有生成器类型,不仅仅是Ninja),这确保了构建工具知道如何正确处理与副产品文件相关的依赖关系细节。对于自定义目标生成文件作为副产品的情况,考虑使用add_custom_command()更适合定义命令和其输出的方式(见下面的第19.3节,“生成文件的命令”)。

对于CMake 3.20或更高版本,对于BYPRODUCTS,支持一组受限制的生成器表达式。任何引用目标的表达式(例如$<TARGET_FILE:…>)都不能使用。

如果命令在控制台上没有输出,有时可以使用COMMENT选项指定一个简短的消息。指定的消息在运行命令之前被记录,因此如果命令由于某种原因静默失败,该注释可以是一个有用的标记,指示构建失败的位置。然而,请注意,对于某些生成器,注释将不会显示,因此这不能被认为是一种可靠的机制,但对于支持它的那些生成器可能仍然有用。一个普遍支持的替代方案在下面的第19.5节,“平台无关命令”中介绍。

USES_TERMINAL是另一个与控制台相关的选项,它指示CMake尽可能地让命令直接访问终端。在使用Ninja生成器时,这会将命令放置在控制台池中。在某些情况下,这可能会导致更好的输出缓冲行为,例如帮助IDE环境捕获和及时呈现构建输出。如果需要对非IDE构建进行交互式输入,这也可能很有用。USES_TERMINAL选项支持CMake 3.2及更高版本。

为了在Ninja作业池上提供更多控制,CMake 3.15添加了对JOB_POOL选项的支持。虽然USES_TERMINAL将任务分配给控制台作业池,但JOB_POOL选项允许项目将任务分配给任何自定义作业池。USES_TERMINAL和JOB_POOL不能同时给定。有关在Ninja中使用作业池的更多信息,请参见第35.3.2节,“Ninja生成器”。

SOURCES选项允许列出任意文件,然后将其与自定义目标关联起来。这些文件可能被命令使用,也可能只是与目标松散关联的一些附加文件,例如文档等。使用SOURCES列出文件不会影响构建或依赖关系,它纯粹是为了将这些文件与目标关联起来,以便IDE项目可以在适当的上下文中显示它们。这个特性有时被定义一个虚拟的自定义目标,并列出没有命令的源文件来利用。虽然这样做有效,但它的缺点是创建了一个没有实际含义的构建目标。许多项目认为这是一个可以接受的折衷,而一些开发人员则认为这是不可取的,甚至是一种反模式。

19.2. 向现有目标添加构建步骤

有时自定义命令不需要定义新目标,而是可以指定在构建现有目标时执行的附加步骤。这就是应该使用add_custom_command()与TARGET关键字的地方,如下所示:

add_custom_command(TARGET targetName buildStage
 COMMAND command1 [args1...]
 [COMMAND command2 [args2...]]
 [WORKING_DIRECTORY dir]
 [BYPRODUCTS files...]
 [COMMENT comment]
 [VERBATIM]
 [USES_TERMINAL]
 # Requires CMake 3.2 or later
 [JOB_POOL poolName] # Requires CMake 3.15 or later
)

大多数选项与add_custom_target()的选项非常相似,但上述形式不是定义一个新目标,而是将命令附加到现有目标上。该现有目标可以是可执行文件或库目标,甚至可以是一个自定义目标(带有一些限制)。命令将作为构建targetName的一部分执行,其中buildStage参数必须是以下之一:

PRE_BUILD

在指定目标的任何其他规则之前应运行命令。请注意,只有Visual Studio生成器支持此选项,并且仅适用于Visual Studio 7或更高版本。所有其他CMake生成器都将其视为PRE_LINK。鉴于对此选项的有限支持,项目应该遵循一种不需要PRE_BUILD自定义命令的结构,以避免不同生成器之间的命令顺序差异。

PRE_LINK

在编译源代码之后但在链接之前运行命令。对于静态库目标,命令将在库存档工具之前运行。对于自定义目标,不支持PRE_LINK。

POST_BUILD

在指定目标的所有其他规则之后运行命令。所有目标类型和生成器都支持此选项,使其成为每当有选择时的首选构建阶段。POST_BUILD任务相对常见,但很少需要PRE_LINK和PRE_BUILD,因为通常可以通过使用add_custom_command()的OUTPUT形式来避免它们(见下一节)。

可以多次调用add_custom_command()将多组自定义命令附加到特定目标。这可能很有用,例如,有些命令可以从一个工作目录运行,而其他命令可以从另一个地方运行。

add_executable(MyExe main.cpp)
add_custom_command(TARGET MyExe POST_BUILD
 COMMAND script1 $<TARGET_FILE:MyExe>
)
# Additional command which will run after the above from a different directory
add_custom_command(TARGET MyExe POST_BUILD
 COMMAND writeHash $<TARGET_FILE:MyExe>
 BYPRODUCTS ${CMAKE_BINARY_DIR}/verify/MyExe.md5
 WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/verify
)

19.3. 生成文件的命令

为目标定义命令作为额外的构建步骤涵盖了许多常见用例。然而,有时项目需要通过运行一个或多个命令来创建一个或多个文件,而生成该文件不真正属于任何现有目标。这就是add_custom_command()的OUTPUT形式可以使用的地方。它实现了与TARGET形式相同的所有选项,以及一些与依赖处理和附加到前一个OUTPUT命令集相关的附加选项。

与指定目标和预/后构建阶段不同,这种形式要求在OUTPUT关键字之后给出一个或多个输出文件名。然后,CMake将解释这些命令为生成命名输出文件的配方。如果输出文件没有指定路径或者使用相对路径,它们将相对于当前的二进制目录。在CMake 3.20或更高版本中,可以使用不引用目标的生成器表达式。

独立使用这种形式不会导致输出文件被构建,因为没有定义目标。然而,如果同一目录范围内定义的其他目标依赖于任何输出文件,CMake将自动创建依赖关系,以确保输出文件在需要它们的目标之前生成。一个常见的错误是试图使不同目录范围内定义的目标依赖于add_custom_command()的输出,但是这是不支持的。此外,只有一个目标应该依赖于任何输出文件,否则并行构建可能会尝试同时多次调用自定义命令,以满足多个目标的依赖关系。

依赖于add_custom_command()的输出的目标可以是普通的可执行文件、库目标,甚至可以是自定义目标。事实上,定义自定义目标仅仅是为了提供一种触发自定义命令的方式是非常常见的。前面章节哈希示例的变体演示了这种技术:

add_executable(MyExe main.cpp)
# Output file with relative path, generated in the build directory
add_custom_command(OUTPUT MyExe.md5
 COMMAND writeHash $<TARGET_FILE:MyExe>
)
# Absolute path needed for DEPENDS, otherwise relative to source directory
add_custom_target(ComputeHash
 DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/MyExe.md5
)

以这种方式定义,构建MyExe目标不会导致运行哈希步骤,与之前将哈希命令作为MyExe目标的POST_BUILD步骤添加的示例不同。相反,只有当开发人员显式请求它作为构建目标时,才会执行哈希。这允许定义和在需要时调用可选步骤,而不是总是运行,这在额外步骤耗时或不总是相关时非常有用。

当然,add_custom_command()也可以用于生成现有目标消耗的文件,例如生成源文件。在以下示例中,项目构建的可执行文件用于生成随后作为另一个可执行文件的一部分编译的源文件。

add_executable(Generator generator.cpp)
add_custom_command(OUTPUT onTheFly.cpp 
 COMMAND Generator
)

add_executable(MyExe ${CMAKE_CURRENT_BINARY_DIR}/onTheFly.cpp)

CMake会自动识别MyExe需要由自定义命令生成的源文件,而这又需要Generator可执行文件。要求构建MyExe目标将导致在构建MyExe之前构建Generator和生成的源文件。然而,请注意,这种依赖关系有一些限制。考虑以下情景:

  • onTheFly.cpp文件最初不存在。
  • 构建MyExe目标,导致以下顺序发生:
    • Generator目标被更新。
    • 执行自定义命令以创建onTheFly.cpp。
    • 构建MyExe目标。
  • 现在修改generator.cpp文件。
  • 再次构建MyExe目标,这次导致以下顺序发生:
    • Generator目标被更新。这将导致重新构建Generator可执行文件,因为其源文件已修改。
    • 不执行自定义命令,因为onTheFly.cpp已经存在。
    • 不重新构建MyExe目标,因为其源文件保持不变。

一个直觉上的期望是,如果重新构建了Generator目标,那么自定义命令也应该重新运行。CMake自动创建的依赖关系并不强制执行这一点,它创建了一个较弱的依赖关系,确保了Generator是最新的,但是只有当输出文件完全缺失时,自定义命令才会运行。为了强制重新运行自定义命令,如果重新构建了Generator目标,必须显式指定一个依赖关系,而不是依赖于CMake自动创建的依赖关系。

可以使用DEPENDS选项手动指定依赖项。在DEPENDS中列出的项目可以是CMake目标或文件(与add_custom_target()的DEPENDS选项相比,后者只能列出文件)。如果列出了一个目标,那么每当需要将自定义命令的输出文件更新为最新时,该目标将被更新为最新。同样,如果列出的文件被修改,如果任何东西需要自定义命令的任何输出文件,那么自定义命令将被执行。

此外,如果任何列出的文件本身是同一目录范围内另一个自定义命令的输出文件,那么将首先执行该其他自定义命令。与add_custom_target()一样,如果在DEPENDS中列出一个文件,请始终使用绝对路径,以避免模糊的旧特性。

虽然CMake的自动依赖关系可能看起来很方便,但实际上,项目仍然通常需要在DEPENDS部分列出所有所需的目标和文件,以确保充分指定了完整的依赖关系。由于第一次构建将运行自定义命令以创建缺失的输出文件,并且构建将表现出正确的行为,因此可能会无意中省略DEPENDS部分。如果重新构建了自动检测到的任何依赖目标,即使输出文件被删除,后续的构建也不会重新运行自定义命令。这很容易被忽略,在复杂项目中通常很长时间不被发现,直到开发人员遇到这种情况并尝试弄清楚为什么某些内容没有被重新构建时才会出现。因此,开发人员应该预期,除非自定义命令不需要由构建创建的任何内容或项目的任何源文件,否则通常需要一个DEPENDS部分。

另一个常见的错误是不创建对自定义命令所需但未列为要执行的命令行的文件的依赖关系。这些文件需要出现在DEPENDS部分中,以使构建被视为健壮。

add_custom_command()还支持几个与依赖关系相关的选项。MAIN_DEPENDENCY选项用于标识应将哪个源文件视为自定义命令的主要依赖项。它与列出的文件的DEPENDS选项几乎具有相同的效果,但是一些生成器可能会应用额外的逻辑,例如在IDE项目中放置自定义命令的位置。需要注意的一个重要区别是,如果将源文件列为MAIN_DEPENDENCY,则自定义命令将成为通常情况下将如何编译该源文件的替代品。这可能会导致一些意外的结果。考虑以下示例:

add_custom_command(OUTPUT transformed.cpp
 COMMAND transform
     ${CMAKE_CURRENT_SOURCE_DIR}/original.cpp
     transformed.cpp
 MAIN_DEPENDENCY
     ${CMAKE_CURRENT_SOURCE_DIR}/original.cpp
)

add_executable(Original original.cpp)
add_executable(Transformed transformed.cpp)

上述示例将导致链接器错误,因为原始目标将不会将original.cpp编译为对象文件,因此根本不会有对象文件(因此也没有main()函数)。相反,构建工具将把original.cpp视为用于创建transformed.cpp的输入文件。该问题可以通过使用DEPENDS而不是MAIN_DEPENDENCY来解决,因为这将保持相同的依赖关系,但不会替换original.cpp源文件的默认编译规则。

另外两个与依赖关系相关的选项IMPLICIT_DEPENDS和DEPFILE并不是所有项目生成器普遍支持的。IMPLICIT_DEPENDS指示CMake调用C或C++扫描程序来确定列出的文件的依赖关系。对于除Makefile生成器之外的所有生成器,它都会被忽略,因此项目通常应避免使用它,如果其他替代方法可用于表示所需的依赖关系。DEPFILE可用于提供*.d依赖文件(由项目负责生成),但是直到CMake 3.19,只有Ninja生成器支持它。从CMake 3.20开始,DEPFILE也可以与Makefile生成器一起使用,而CMake 3.21添加了对Xcode和Visual Studio生成器的支持。尽管依赖文件有其用途,但它们更复杂,对于大多数典型项目而言,它们不需要手动管理。IMPLICIT_DEPENDS和DEPFILE不能一起使用。

CMake 3.20还引入了与DEPFILE相关的另一个变化,这可能会影响使用较早版本的CMake的项目。CMake 3.20添加了策略CMP0116,与大多数策略不同,即使项目没有调用或依赖于旧行为,也可能会导致警告。警告引起了相对路径的处理方式的变化,当DEPFILE与add_custom_command()的调用一起使用时,除了在顶层源目录之外的任何地方。CMake不能可靠地检查depfile的内容,因为它通常在构建时更新。因此,除非项目已更新以确保策略CMP0116设置为NEW,否则它会保守地发出警告。使用CMake 3.19或更早版本的DEPFILE的项目应进行检查,以确保根据CMP0116策略的要求使用绝对路径。然后,可以通过调整传递给cmake_minimum_required()调用的版本范围全局地或局部地进行更新,以避免警告。下面的示例演示了如何在不影响最低CMake版本要求的情况下局部调整策略设置(有关详细信息,请参见第12章,策略):

# Give ourselves a local policy set we can safely modify
cmake_policy(PUSH)
# Use the NEW policy setting only if it is available
if(POLICY CMP0116)
 cmake_policy(SET CMP0116 NEW)
endif()
# We guarantee that depfile.d will not use relative paths
# for any dependency it specifies
add_custom_command(OUTPUT ...
 DEPFILE /some/absolute/path/to/depfile.d
 ...
)
# Restore the original policy settings
cmake_policy(POP)

OUTPUT和TARGET形式在追加更多的依赖项或命令到相同的输出文件或目标时,也有稍微不同的行为。对于OUTPUT形式,必须指定APPEND关键字,并且第一个OUTPUT文件的列出的文件必须与第一个和后续调用的add_custom_command()相同。对于同一个输出文件的第二个和后续调用,只能使用COMMAND和DEPENDS。当存在APPEND关键字时,其他选项(如MAIN_DEPENDENCY、WORKING_DIRECTORY和COMMENT)将被忽略。相比之下,对于TARGET形式,对于同一目标的第二个和后续调用的add_custom_command(),不需要APPEND关键字。对于每次调用,都可以指定COMMENT和WORKING_DIRECTORY选项,它们将对该调用中添加的命令产生影响。

19.4. 配置时任务

add_custom_target()和add_custom_command()都定义了在构建阶段执行的命令。通常情况下,这是运行自定义命令的时候,但有些情况下,自定义任务需要在配置阶段执行。一些需要这样做的例子包括:

  • 执行外部命令以获取配置期间使用的信息。命令输出通常直接捕获到CMake变量中以供进一步处理。
  • 编写或更新(touch)需要在重新运行CMake时更新的文件。
  • 生成CMakeLists.txt或其他文件,这些文件需要作为当前配置步骤的一部分被包含或处理。

CMake提供了execute_process()命令来在配置阶段运行此类任务:

execute_process( 
COMMAND command1 [args1...]
[COMMAND command2 [args2...]]
[WORKING_DIRECTORY directory]
[RESULT_VARIABLE resultVar]
[RESULTS_VARIABLE resultsVar]
[OUTPUT_VARIABLE outputVar]
[ERROR_VARIABLE errorVar]
[OUTPUT_STRIP_TRAILING_WHITESPACE]
[ERROR_STRIP_TRAILING_WHITESPACE]
[INPUT_FILE inFile]
[OUTPUT_FILE outFile]
[ERROR_FILE errorFile]
[OUTPUT_QUIET]
[ERROR_QUIET]
[TIMEOUT seconds]
 
 
# CMake 3.15 or later required:
[COMMAND_ECHO STDOUT | STDERR | NONE]
 
 
 
# CMake 3.18 or later required:
[ECHO_OUTPUT_VARIABLE]
[ECHO_ERROR_VARIABLE]
 
 
)
# CMake 3.19 or later required:
[[COMMAND_ERROR_IS_FATAL ANY | LAST]
)

与add_custom_command()和add_custom_target()类似,一个或多个COMMAND部分指定要执行的任务,WORKING_DIRECTORY选项可用于控制命令的运行位置。命令以原样传递给操作系统执行,没有中间的shell环境。因此,不支持输入/输出重定向和环境变量等功能。命令会立即运行。 如果给出了多个命令,则按顺序执行它们,但是它们并非完全独立,而是将一个命令的标准输出管道传递给下一个命令的输入。在没有其他选项的情况下,最后一个命令的输出被发送到CMake进程本身的输出,但是每个命令的标准错误则被发送到CMake进程的标准错误流。

标准输出和标准错误流可以被捕获并存储在变量中,而不是被发送到默认的管道。通过使用OUTPUT_VARIABLE选项指定一个变量名称来将一组命令中最后一个命令的输出捕获到其中。类似地,所有命令的标准错误流可以存储在ERROR_VARIABLE选项指定的变量中。将相同的变量名称传递给这两个选项将导致标准输出和标准错误被合并,就像它们被输出到终端一样,合并后的结果被存储在命名变量中。使用CMake 3.18或更高版本,可以添加ECHO_OUTPUT_VARIABLE和ECHO_ERROR_VARIABLE选项来回显输出和错误流,同时将它们捕获到变量中。对于运行时间较长的命令,查看输出中的进度有助于确认命令没有挂起。

如果存在OUTPUT_STRIP_TRAILING_WHITESPACE选项,则将从输出变量中存储的内容中省略任何尾随空白。ERROR_STRIP_TRAILING_WHITESPACE选项对于错误变量中存储的内容执行类似的操作。如果将输出或错误变量的内容用于任何字符串比较,常见的问题是未考虑尾随空白,因此通常希望移除它。

而不是将输出和错误流捕获到变量中,它们可以被发送到文件中。OUTPUT_FILE和ERROR_FILE选项可用于指定要发送流的文件的名称。与变量焦点的选项类似,指定相同的文件名将导致合并流。此外,可以使用INPUT_FILE选项将文件指定为第一个命令的输入流。但是,请注意,OUTPUT_STRIP_TRAILING_WHITESPACE和ERROR_STRIP_TRAILING_WHITESPACE选项不会对发送到文件的内容产生影响。当捕获到文件时,也没有能力回显输出或错误流。

同一流不能同时捕获到变量并发送到文件中。然而,可以将不同的流发送到不同的位置,例如将输出流发送到变量中,将错误流发送到文件中,反之亦然。还可以通过使用OUTPUT_QUIET和ERROR_QUIET选项来静默丢弃流的内容。如果只关心命令的成功或失败,这些选项可能很有用。

命令集的成功或失败可以使用RESULT_VARIABLE选项进行捕获。运行命令的结果将存储在指定的变量中,作为最后一个命令的整数返回代码或包含某种错误消息的字符串。if()命令方便地将非空错误字符串和非零的整数值都视为布尔值true(除非项目不幸地具有满足一些特殊情况的错误字符串,请参见第6.1.1节,“基本表达式”)。因此,通常相对简单地检查对execute_process()的调用的成功性:

execute_process(
 COMMAND runSomeScript
 RESULT_VARIABLE result
)
if(result)
 message(FATAL_ERROR "runSomeScript failed: ${result}")
endif()

从CMake 3.10开始,如果需要每个单独命令的结果而不仅仅是最后一个命令的结果,可以使用RESULTS_VARIABLE选项。此选项将每个命令的结果存储在resultsVar命名的变量中,作为列表。

使用CMake 3.19或更高版本,可以使用COMMAND_ERROR_IS_FATAL选项作为更简洁的方法,在命令失败时终止并报错。它避免了接收结果到变量并执行显式检查的需要,因为检查是由execute_process()命令直接执行的。该选项必须紧跟着ANY或LAST。当给出多个COMMAND时,指定ANY将导致execute_process()命令在任何命令失败时失败并显示致命错误。如果指定LAST,则execute_process()仅在最后一个COMMAND失败时失败。如果只给出一个COMMAND,则ANY和LAST是等效的。

# Automatically halt with an error if either command fails
execute_process(
 COMMAND runSomeScript
 COMMAND runSomethingElse
 COMMAND_ERROR_IS_FATAL ANY
)

TIMEOUT选项可用于处理可能运行时间比预期长或可能永远不会完成的命令。这确保了配置步骤不会无限期地阻塞,并允许将意外长时间的配置步骤视为错误。但是,请注意,单独使用TIMEOUT选项不会导致CMake停止并报告错误。仍然需要使用RESULT_VARIABLE和if()测试来检查结果,或提供COMMAND_ERROR_IS_FATAL选项。请注意,在初始的CMake 3.19.0版本中,如果一个命令超时,它不会被COMMAND_ERROR_IS_FATAL选项捕获和视为致命错误。这个问题在CMake 3.19.2中已经修复,所以如果使用这个选项,请考虑这是最低要求的CMake版本。如果使用RESULT_VARIABLE方法,结果变量将保存一个错误字符串,指示由于超时而终止的命令运行时间过长,因此在错误消息中打印它是有用的。

CMake 3.15添加了对COMMAND_ECHO选项的支持,该选项必须跟随STDOUT、STDERR或NONE之一。这控制在哪里回显每个COMMAND(命令行本身,而不是命令的输出),或者在NONE的情况下,阻止命令被回显。如果没有指定COMMAND_ECHO选项,则默认行为由CMAKE_EXECUTE_PROCESS_COMMAND_ECHO变量确定,该变量支持相同的三个值。如果该变量也未定义,或者CMake版本为3.14或更早版本,则不会回显命令。

当CMake执行命令时,子进程在很大程度上继承了与主进程相同的环境。然而,CMake 3.23及更早版本存在一个重要的例外情况。在项目首次运行CMake时,子进程的CC和CXX环境变量会明确设置为主构建中使用的C和C++编译器(如果主项目启用了C和C++语言)。对于后续的CMake运行,CC和CXX环境变量不会以这种方式替换。如果命令执行动作依赖于每次调用execute_process()时CC和CXX环境变量具有相同值,这可能会导致意外的结果。在CMake 3.24之前,这种行为未记录,但它自CMake早期版本以来一直存在,甚至包括现在已经弃用的exec_program()命令,它被execute_process()取代。这种行为的添加是为了让子进程能够配置和运行与主项目相同编译器的子构建。然而,在某些情况下,子进程可能不希望保留编译器,比如当主构建进行交叉编译时,而子进程应该使用默认的主机编译器。CMake 3.24引入了策略CMP0132,当它设置为NEW时避免上述行为。当使用CMake 3.23或更早版本时,项目可以将一个未记录的变量CMAKE_GENERATOR_NO_COMPILER_ENV设置为布尔值true,其效果与将CMP0132设置为NEW相同。

19.5. 平台无关命令

add_custom_command()、add_custom_target() 和 execute_process() 命令为项目提供了很大的自由度。任何在 CMake 中尚未直接支持的任务都可以使用主机操作系统提供的命令来实现。这些自定义命令是平台特定的,这与许多项目最初使用 CMake 的主要原因之一相悖,即将平台差异抽象化,或至少以最小的努力支持一系列平台。

大部分自定义任务与文件系统操作相关。创建、删除、重命名或移动文件和目录构成了这些任务的大部分内容,但是执行这些任务的命令在不同的操作系统之间是不同的。因此,项目通常会使用 if-else 条件来定义相同命令的不同平台版本,或者更糟糕的是,他们只会为某些平台实现这些命令。许多开发人员并不知道 CMake 提供了一种命令模式,可以将许多这些平台特定的任务抽象化:

cmake -E cmd [args...]

可以使用 cmake -E help 来列出支持的所有命令,但一些常用的命令包括 copy、copy_if_different、echo、env、make_directory、md5sum、rm 和 tar。

考虑一个自定义任务的例子,用于删除特定目录及其所有内容:

set(discardDir "${CMAKE_CURRENT_BINARY_DIR}/private")
# Naive platform specific implementation (not robust)
if(WIN32)
 add_custom_target(MyCleanup COMMAND rmdir /S /Q "${discardDir}")
elseif(UNIX)
 add_custom_target(MyCleanup COMMAND rm -rf "${discardDir}")
else()
 message(FATAL_ERROR "Unsupported platform")
endif()
# Platform independent equivalent
add_custom_target(MyCleanup COMMAND "${CMAKE_COMMAND}" -E rm -R "${discardDir}")

平台特定的实现显示了项目通常尝试实现这种情况,但是 if-else 条件测试的是目标平台而不是主机平台。在交叉编译的情况下,可能会使用错误的命令。平台无关版本没有这样的弱点。它始终为主机平台选择正确的命令。

示例还显示了如何正确调用 cmake 命令。CMAKE_COMMAND 变量由 CMake 填充,其中包含了用于主构建中使用的 cmake 可执行文件的完整路径。以这种方式使用 CMAKE_COMMAND 可确保自定义命令也使用相同版本的 CMake。cmake 可执行文件不必位于当前的 PATH 中,如果安装了多个版本的 CMake,则始终使用正确的版本,而不管用户的 PATH 环境变量选择了哪一个版本。它还确保在构建阶段使用与配置阶段相同的 CMake 版本,即使用户的 PATH 环境变量发生了变化。

在本章的前面部分已经注意到,add_custom_target() 和 add_custom_command() 的 COMMENT 选项并不总是可靠的。项目可以使用 -E echo 命令在自定义命令序列的任何位置插入注释:

set(discardDir "${CMAKE_CURRENT_BINARY_DIR}/private")
add_custom_target(MyCleanup
 COMMAND ${CMAKE_COMMAND} -E echo "Removing ${discardDir}"
 COMMAND ${CMAKE_COMMAND} -E rm -R "${discardDir}"
 COMMAND ${CMAKE_COMMAND} -E echo "Recreating ${discardDir}"
 COMMAND ${CMAKE_COMMAND} -E make_directory "${discardDir}"
)

CMake 的命令模式是以一种非常有用的方式以平台无关的方式执行一系列常见任务。然而,有时需要更复杂的逻辑,这样的自定义任务通常使用特定于平台的 shell 脚本来实现。另一种选择是将 CMake 本身用作脚本引擎,提供了一种平台无关的语言来表达任意逻辑。cmake 命令的 -P 选项将 CMake 置于脚本处理模式:

cmake [options] -P filename

filename 参数是要执行的 CMake 脚本文件的名称。支持通常的 CMakeLists.txt 语法,但没有配置或生成步骤,也不会更新 CMakeCache.txt 文件。脚本文件实际上只是作为一组命令进行处理,而不是作为一个项目,因此不支持与构建目标或项目级功能相关的任何命令。

尽管脚本模式不支持像普通 shell 或命令解释器那样的命令行选项,但它支持使用 -D 选项传递变量,就像普通的 cmake 调用一样。由于在脚本模式下不会更新 CMakeCache.txt 文件,因此可以自由使用 -D 选项,而不会影响主构建的缓存。这些选项必须放在 -P 之前。

cmake -DOPTION_A=1 -DOPTION_B=foo -P myCustomScript.cmake

19.6. 结合不同方法

下面的示例演示了本章讨论的许多特性。它展示了如何以不同的方式指定自定义任务。示例的一个重要方面是它如何在不必诉诸于特定于平台的命令或功能的情况下完成非平凡的任务。

通过将打包逻辑放在单独的 archiver.cmake 文件中,它可以在项目内部使用,如所示,也可以通过 CMake 的脚本模式单独调用。这对于开发和测试来说可能很有用,或者为了提供一个在任何项目之外普遍使用的平台无关的打包目录的方式。

CMakeLists.txt:

cmake_minimum_required(VERSIONproject(Example)
3.17)
# This executable generates files in a directory passed as a command line argument
add_executable(GenerateFiles generateFiles.cpp)
# Custom target to run the above executable and archive its results
set(outDir "foo")
add_custom_target(Archiver
 COMMAND ${CMAKE_COMMAND} -E echo "Archiving files"
 COMMAND ${CMAKE_COMMAND} -E rm -R "${outDir}"
 COMMAND ${CMAKE_COMMAND} -E make_directory "${outDir}"
 COMMAND GenerateFiles "${outDir}"
 COMMAND ${CMAKE_COMMAND} "-DTAR_DIR=${outDir}"
 -P "${CMAKE_CURRENT_SOURCE_DIR}/archiver.cmake"
)

archiver.cmake:

cmake_minimum_required(VERSION 3.17)
if(NOT TAR_DIR)
 message(FATAL_ERROR "TAR_DIR must be set")
endif()
# Create an archive of the directory
set(archive archive.tar)
execute_process(
 COMMAND ${CMAKE_COMMAND} -E tar cf ${archive} "${TAR_DIR}"
 RESULT_VARIABLE result
)
if(result)
 message(FATAL_ERROR "Archiving ${TAR_DIR} failed: ${result}")
endif()
# Compute MD5 checksum of the archive
execute_process(
 COMMAND ${CMAKE_COMMAND} -E md5sum ${archive}
 OUTPUT_VARIABLE md5output
 RESULT_VARIABLE result
)
if(result)
 message(FATAL_ERROR "Unable to compute md5 of archive: ${result}")
endif()
# Extract just the checksum from the output
string(REGEX MATCH "^ *[^ ]*" md5sum "${md5output}")
message("Archive MD5 checksum: ${md5sum}")

19.7. 推荐做法

当需要执行自定义任务时,最好在构建阶段而不是配置阶段执行。快速的配置阶段很重要,因为当某些文件被修改时(例如项目中的任何 CMakeLists.txt 文件、由 CMakeLists.txt 文件包含的任何文件或作为 configure_file() 命令的源文件的任何文件,如下一章所述),它可以自动调用。因此,如果有选择的话,最好使用 add_custom_target() 或 add_custom_command(),而不是 execute_process()。如果任务需要立即运行或作为 cmake -P 脚本模式调用的一部分,则可以使用 execute_process()。

通常会看到在 add_custom_command()、add_custom_target() 和 execute_process() 中使用特定于平台的命令。然而,很多时候,这样的命令可以通过使用 CMake 的命令模式(-E)以平台无关的方式来表达。在可能的情况下,应优先使用平台无关的命令。此外,CMake 可以用作平台无关的脚本语言,在被 -P 选项调用时,将文件处理为一系列的 CMake 命令。使用 CMake 脚本而不是特定于平台的 shell 或单独安装的脚本引擎可以减少项目的复杂性,并减少其构建所需的额外依赖。具体来说,要考虑使用 CMake 的脚本模式是否比使用 Unix shell 脚本或 Windows 批处理文件,甚至是像 Python、Perl 等语言的脚本更好,因为这些语言可能不会默认在所有平台上可用。下一章将展示如何直接使用 CMake 操作文件,而不必诉诸于这样的工具和方法。

在实现自定义任务时,尽量避免在所有情况下都缺乏支持的特性:

  • 与 add_custom_command() 和 add_custom_target() 一起使用 COMMAND 模式 -E echo,而不是 COMMENT 关键字。
  • 尽量避免在 add_custom_command() 的 TARGET 形式中使用 PRE_BUILD。
  • 考虑在 add_custom_command() 中使用 IMPLICIT_DEPENDS 或 DEPFILE 选项是否值得为生成器特定的行为。
  • 除非意图是替换该源文件的默认构建规则,否则避免在 add_custom_command() 中将源文件列为 MAIN_DEPENDENCY。

特别注意自定义任务的输入和输出的依赖关系。确保由 add_custom_command() 创建的所有文件都被列为 OUTPUT 文件。在调用 add_custom_command() 或 add_custom_target() 中的命令或参数时,将构建目标作为 DEPENDS 项显式列出,而不是依赖于 CMake 的自动目标依赖处理。

在调用 execute_process() 时,大多数情况下,应通过使用 RESULT_VARIABLE 并使用 if() 命令测试其来测试命令的成功与否。这包括使用 TIMEOUT 选项时,因为单独使用 TIMEOUT 不会生成错误,它只会确保命令不会运行超过指定的超时期限。

在某些类型的项目中,使用优化目标与使用非优化目标执行自定义命令之间的差异可能会对构建时间产生明显的影响。一个常见的例子是使用自定义命令来运行代码生成器,这些代码生成器本身作为项目的一部分构建。如果代码生成是一个非平凡的过程,那么可能希望在构建其余项目为 Debug 时,代码生成器也以 Release 配置构建。CMake 3.17 引入的 Ninja Multi-Config 生成器是唯一直接支持此工作流程的生成器。考虑到这个生成器的相对较新的状态,应谨慎考虑在很大程度上依赖此功能之前。感兴趣的读者应查阅 CMake 文档,了解有关 Ninja Multi-Config 生成器的此功能和其他相关高级功能的详细信息。