第17章:自定义任务

没有一个构建工具能够实现给定项目所需要的所有特性。某些情况下,开发人员将需要执行超出目前支持功能的任务。例如,可能需要运行一个特殊的工具来生成源文件,或者在生成目标之后对其进行处理。可能需要复制、验证文件或计算哈希值。可能需要归档构建或联系通知服务。这些任务并不总是可预测的,该模式允许它们可以作为通用构建系统功能提供。

CMake通过自定义命令和自定义目标来支持任务。这些命令允许在构建时执行任何命令(或一组命令),以执行项目所需的任何任务。CMake还支持在配置时执行任务,支持各种技术,这些技术依赖于在构建阶段之前完成的任务,甚至在处理当前CMakeLists.txt文件的后面部分前完成的任务。

17.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]
 [SOURCES source1 [source2...]]
)

具有指定targetName的新目标将对生成可用。ALL选项使所有目标依赖于这个新的自定义目标(各种生成器对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会自动在该可执行目标上创建依赖项,以确保它是在自定义目标前构建。类似地,如果在生成器表达式中命令或参数的任何位置引用了目标,也会自动在该目标上创建依赖项。如果需要指定任何其他目标上的依赖项,可以使用add_dependencies()命令来定义该关系。如果依赖关系存在于文件而不是目标上,则可以使用DEPENDS关键字直接在add_custom_target()中指定该关系。注意,依赖关系不应用于目标依赖关系,而应仅用于文件依赖关系。当列出的文件是由其他自定义命令生成时,DEPENDS关键字尤其有用(参见17.3节“生成文件的命令”),其中CMake将设置必要的依赖关系,以确保其他自定义命令在这个自定义目标的命令前执行。对于DEPENDS始终使用绝对路径,由于遗留特性允许对多个位置进行路径匹配,所以相对路径可能会给出意外结果。

提供多个命令时,每一个都会列出的顺序执行。但是,项目不应该假定任何shell行为,因为每个命令可能在自己单独的shell中运行,或者根本不需要任何shell环境。自定义命令应该定义为独立执行的,没有任何外壳特性(如重定向、变量替换等)的,只是按顺序执行命令的。虽然其中一些特性可以在某些平台上工作,但并没有得到普遍的支持。另外,由于没有保证特定的shell行为,在不同的平台上对可执行名称或参数进行转义可能会有不同的处理方式。为了减少这些差异,可以使用VERBATIM选项来确保在解析CMakeLists.txt文件时,只有CMake的部分进行了转义。平台不再执行转义操作,因此开发人员可以对命令的最终构造方式有信心。如果可以避免相关性,建议使用VERBATIM关键字。

默认情况下,执行命令的目录是当前二进制目录。这可以通过WORKING_DIRECTORY选项进行更改,该选项可以是绝对路径,也可以是相对路径,后者是相对于当前二进制目录的。这意味着不需要使用${CMAKE_CURRENT_BINARY_DIR}作为工作目录的一部分。

可以使用BYPRODUCTS选项列出在运行命令时创建的其他文件。如果使用Ninja生成器,如果运行这组自定义命令的需要另一个目标创建的任何文件,则需要此选项。作为输出的文件标记为已生成(对于所有生成器类型,而不仅仅是Ninja),这确保构建工具知道如何正确处理与输出文件相关的依赖。对于自定义目标将文件作为输出生成的情况,请考虑使用add_custom_command()是否是定义命令及其输出内容更合适的方法(参见17.3节“生成文件的命令”)。

如果在控制台上的命令不产生输出,使用COMMENT选项指定一条短消息会很有用。指定的消息在运行命令前会记录下来,因此如果命令由于某种原因静默地失败,注释可以作为一个有用的标记,指示构建失败的位置。但是请注意,对于一些生成器,注释将不会显示,因此这不能认为是一种可靠的机制,但是对于那些支持注释的生成器,它可能仍然有用。17.5节“平台独立命令”中给出了一个普遍支持的替代方案。

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

SOURCES选项允许列出任意文件,然后将这些文件与自定义目标相关联。这些文件可能在命令中使用,也可能只是一些与目标没太大关联的附加文件,比如文档等等。列出带有源的文件对构建或依赖关系没有影响,这纯粹是为了将这些文件与目标进行关联,以便IDE可以在适当显示它们。这个特性有时可以通过定义一个虚拟的自定义目标和不带命令的源,在IDE中显示。虽然这样做是可行的,但缺点是构建的目标没有真正的意义。许多项目认为这是一个可接受的折衷,有些开发人员认为这是一种反模式。

17.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]
)

大多数选项与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()的输出形式来避免(参见下一节)。

可以对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
)

17.3. 生成命令

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

add_custom_command(OUTPUT output1 [output2...]
 COMMAND command1 [args1...]
 [COMMAND command2 [args2...]]
 [WORKING_DIRECTORY dir]
 [BYPRODUCTS files...]
 [COMMENT comment]
 [VERBATIM]
 [USES_TERMINAL]
 [APPEND]
 [DEPENDS [depends1...]
 [MAIN_DEPENDENCY depend]
 [IMPLICIT_DEPENDS <lang1> depend1
 [<lang2> depend2...]]
 [DEPFILE depfile]
)

与指定目标和构建前/构建后阶段不同,此方式要求在OUTPUT关键字之后提供一个或多个输出文件名。CMake将这些命令解释为生成指定输出文件的方法。如果输出文件没有指定路径或指定了相对路径,则是相对于当前二进制目录。

这种形式不会构建的输出文件,因为没有目标定义。但是,如果同一目录范围内定义的其他目标依赖于任何输出文件,CMake将自动创建依赖关系,以确保输出文件在需要的目标之前生成。该目标可以是普通的可执行文件、库目标,甚至可以是自定义目标。实际上,定义自定义目标只是为开发人员提供一种触发自定义命令的方法。下面对上一节的哈希示例进行了修改,演示了这种技术:

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需要自定义命令生成的源文件,而源文件又需要生成器可执行文件。请求要构建的myExe目标,将在构建myExe之前构建生成器和生成的源文件。但是请注意,这种依赖关系有局限性。考虑以下情况:

  • onTheFly.cpp最初并不存在。

  • 构建myExe目标的结果如下:

    • 生成器目标是新的。

    • 执行自定义命令来创建onTheFly.cpp。

    • 构建myExe目标。

  • 现在修改生generator.cpp文件。

  • 再次构建myExe目标,这一次的结果如下:

    • 生成器目标是最新的。这将导致生成器重新生成可执行文件,因为源文件修改了。

    • 不执行自定义命令,因为onTheFly.cpp已经存在。

    • 没有重新构建myExe目标,因为源文件保持不变。

人们可能会直观地认为,如果重新构建生成器目标,那么也应该重新运行自定义命令。CMake自动创建的依赖项不会强制执行这一点,它创建了一个较弱的依赖项,确保生成器是最新的,但自定义命令只在输出文件完全找不到时运行。为了在生成器目标重建时强制重新运行自定义命令,必须指定显式依赖项,而不是使用CMake自动创建的依赖项。

可以手动指定依赖项决定的选择。使用DEPENDS列出的配置项,可以是CMake目标或文件(与之相比,add_custom_target()的DEPENDS选项只能配置文件)。如果配置是一个目标,需要将自定义命令的输出文件更新时,该目标将更新。类似地,如果修改了配置的文件,如果有任何事情需要自定义命令输出的文件,则将执行自定义命令。此外,如果任何配置文件本身,是同一目录范围内的另一个自定义命令的输出文件,则首先执行另一个自定义命令。对于add_custom_target(),如果DEPENDS的依赖项中有文件,则始终使用绝对路径,以避免不明确的行为。

虽然CMake的自动依赖关系看起来很方便,但在项目通常仍需要在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。除了Makefile生成器外,其他所有生成器都忽略IMPLICIT_DEPENDS。如果使用的不是Ninja生成器,使用DEPFILE会导致错误。IMPLICIT_DEPENDS指示CMake调用C或C++扫描器来确定所列文件的依赖关系,而DEPFILE可生成特定于Ninja的.d依赖关系文件。项目通常要避免这两种选择,因为支持它们生成器数量非常有限。

将更多依赖项或命令附加到同一个输出文件或目标时,OUTPUT和TARGET 的行为也略有不同。对于OUTPUT,必须指定APPEND关键字,并且对于第一次和随后对add_custom_command()的调用,第一个OUTPUT文件必须相同。只有COMMAND和DEPENDS可用于对同一输出文件的第二次调用和后续调用,当存在APPEND关键字时,将忽略其他选项,如MAIN_DEPENDENCY、WORKING_DIRECTORY和COMMENT。对于TARGET ,同一目标的第二次调用和后续调用add_custom_command(),不需要附加关键字。还可以为每次调用指定COMMENT和WORKING_DIRECTORY选项。

17.4. 配置时任务

add_custom_target()和add_custom_command()定义了在构建阶段要执行的命令。这是典型的自定义命令,但也有一些情况下,自定义任务需要在配置阶段执行,例如:

  • 执行外部命令以获取配置期间使用的信息。

  • 更新文件的任何时候重新运行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]
)

与add_custom_command()和add_custom_target()类似,一个或多个命令指定了执行任务,WORKING_DIRECTORY选项可用于控制在何处运行这些命令。命令传递到操作系统执行,没有中间shell环境。因此,不支持输入/输出重定向和环境变量这样的特性。命令会立即运行。

多个命令会按顺序执行它们,但不是完全独立于彼此,而是将命令的标准输出通过管道传输到下一个命令作为输入。没有任何其他选项的情况下,最后一个命令的输出发送到CMake进程本身,但每个命令的标准错误会发送到CMake进程的标准错误流中。

可以捕获标准输出和错误流并存储在变量中。最后一个命令的输出命令的集合可以指定一个变量来存储它在OUTPUT_VARIABLE选项指定的地方。类似地,所有命令的错误流都可以存储在由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()命令将非空的错误字符串和0以外的整数值都处理为布尔值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命名的变量中。

TIMEOUT选项可用于处理可能运行时间超过预期或可能永远无法完成的命令。这确保了配置步骤不会无限期地阻塞,并允许将异常的长时间配置视为错误。但请注意,TIMEOUT选项本身不会让CMake停止并报错。仍然需要使用RESULT_VARIABLE捕获该命令的结果,然后必须检查该变量,如前面的示例所示,如果命令运行的时间超过了超时阈值,result变量将保存一个错误字符串,指示由于超时而终止命令,这就是为什么建议打印result变量的原因。

CMake执行命令时,子进程很大程度上继承了与主进程相同的环境。一个例外是CMake首次运行在项目中,子进程的CC和CXX环境变量显式地设置为C和C++编译器(如果主要项目使得C和C++语言)。对于后续的CMake运行,CC和CXX环境变量不会以这种方式进行替换,如果命令执行的操作依赖于CC和/或每次调用execute_process()时具有相同值的CXX,则可能导致意外结果。这种未文档化的行为早期版本的CMake就已经存在了,甚至可以追溯到现在已被execute_process()替换的exec_program()命令。添加它是为了方便子进程配置和运行与主项目使用相同编译器的子构建。然而,在某些情况下,子进程可能不希望保留编译器,比如当主构建是交叉编译的时候,子进程使用默认的主机编译器。这种情况下,项目可以将名为CMAKE_GENERATOR_NO_COMPILER_ENV的变量设置为布尔true,这样CMake就不会为任何execute_process()调用(甚至是初始调用)设置CC和CXX。

17.5. 平台独立命令

add_custom_command()、add_custom_target()和execute_process()命令为项目提供了很大的自由度。任何CMake没有直接支持的任务都可以使用主机操作系统提供的命令来实现。这些自定义命令本质上适用于特定的平台,对许多项目使用CMake的一个主要原因是为了抽象平台差异,或者至少以最小的成本支持一系列平台

大部分自定义任务都与文件系统操作相关。创建、删除、重命名或移动文件和目录构成了这些任务的大部分,但是执行这些任务的命令因操作系统的不同而有所不同。因此,项目经常使用if-else条件来定义同一命令的不同平台版本,更糟的是,它们只费心为某些平台实现命令。许多开发人员不知道cmake命令本身提供了命令模式,它抽象了许多平台的特定任务:

cmake -E cmd [args...]

支持的完整的命令集可以使用cmake -E help列出,但更常用命令包括:

  • compare_files

  • copy

  • copy_directory

  • copy_if_different

  • echo

  • env

  • make_directory

  • md5sum

  • remove

  • remove_directory

  • rename

  • tar

  • time

  • touch

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

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 remove_directory "${discardDir}"
)

特定于平台的实现显示了项目通常如何尝试实现这样的场景,但是if-else条件测试的是目标平台,而不是主机平台。在交叉编译场景中,这可能会导致使用错误的平台命令。然而,平台独立版本总是为主机平台选择正确的命令。

该示例还展示了如何正确使用cmake命令。CMAKE_COMMAND变量由CMake填充,它包含主构建中使用的CMake可执行文件的完整路径。以这种方式使用CMAKE_COMMAND可以确保定制命令也使用相同版本的CMake。cmake可执行文件不需要在当前路径上,如果安装了多个版本的cmake,总会使用正确的版本,而不管用户的路径选择了哪个版本。即使用户的PATH环境变量发生了变化,它还会在构建阶段使用与在配置阶段相同的CMake版本。

本章的前面,我们注意到add_custom_target()和add_custom_command()的COMMENT选项并不可靠。与使用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 remove_directory "${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

文件名参数是要执行的CMake脚本文件的名称。支持CMakeLists.txt语法,但没有配置或生成步骤,而且不会更新CMakeCache.txt文件。脚本文件本质上只是作为一组命令处理,而不是作为一个项目,因此与构建目标或项目级特性相关的任何命令都不受支持。尽管如此,脚本模式允许实现复杂的逻辑,而且优点是不需要安装任何额外的解释器。

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

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

17.6. 结合不同的方法

下面的例子演示了本章中介绍的许多特性。特别地,展示了如何使用指定自定义任务的不同方法来完成重要的任务,而不必使用特定于平台的命令或功能。

CMakeLists.txt

cmake_minimum_required(VERSION 3.0)
project(Example)

# Define an executable which generates various files in a
# directory passed as a command line argument
add_program(generateFiles generateFiles.cpp)

# Create a custom target which invokes the above executable
# after creating an empty output directory for it to populate,
# then invoke a script to archive that directory's contents
# and print the MD5 checksum of that archive
set(outDir "foo")
add_custom_target(archiver
 COMMAND ${CMAKE_COMMAND} -E echo "Archiving generated files"
 COMMAND ${CMAKE_COMMAND} -E remove_directory "${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.0)

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

17.7. 推荐

需要执行定制任务时,最好在构建阶段而不是配置阶段执行。快速配置阶段很重要,因为可以调用时自动修改一些文件(例如:项目的任何CMakeLists.txt文件,任何文件包含由CMakeLists.txt文件或任何文件列出的来源configure_file()命令会在下一章中讨论)。出于这个原因,如果有选择的话,最好使用add_custom_target()或add_custom_command(),而不是execute_process()。

与平台相关的命令经常与add_custom_command()、add_custom_target()和execute_process()一起使用。但是,这些命令通常可以使用CMake的命令模式(-E)以平台无关的方式表示。可能的情况下,最好使用与平台无关的命令。此外,CMake还可以用作一种独立于平台的脚本语言,当使用-P选项调用CMake命令序列时,可以将文件处理为CMake命令序列。使用CMake脚本,而不是特定于平台的shell或单独安装的脚本引擎,可以降低项目的复杂性,并减少所需的额外依赖。具体来说,考虑CMake的脚本模式是否会比使用Unix shell脚本或Windows批处理文件,甚至是Python、Perl等语言的脚本更好,因为这些语言可能在所有平台上默认都不可用。下一章将展示如何使用CMake直接操作文件,而不必求助于这些工具和方法。

实现自定义任务时,尽量避免那些不能跨平台提供的通用特性。

  • 最好使用命令模式-E echo,而不是使用带有COMMENT关键字的add_custom_command()和add_custom_target()。

  • 尽量避免在add_custom_command()中使用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的自动依赖项处理。较弱的自动依赖关系可能不会强制执行开发人员所期望的操作。如果列表中的文件DEPENDS于add_custom_target()或add_custom_command(),则始终使用绝对路径以避免路径匹配错误。

调用execute_process()时,大多数时候应该通过RESULT_VARIABLE捕获结果,并使用if()命令测试该命令的状态。包括在使用TIMEOUT选项时,因为TIMEOUT本身不会产生错误,所以只会确保命令运行的时间不会超过指定的超时时间。

Last updated