自定义命令

通常,软件项目的构建过程不仅包括编译库和可执行文件。在许多情况下,在构建过程期间或之后可能需要额外的任务。常见的示例包括:使用文档包编译文档;通过运行另一个可执行文件生成源文件;使用 CMake 没有规则的工具生成文件(如 lex 和 yacc);移动生成的可执行文件;对可执行文件进行后处理等。CMake 支持使用 add_custom_commandadd_custom_target 命令来执行这些其他任务。本章将描述如何使用自定义命令和目标来执行 CMake 本身不支持的复杂任务。

可移植的自定义命令

在详细介绍如何使用自定义命令之前,我们将探讨如何解决其中的一些可移植性问题。自定义命令通常涉及运行具有文件作为输入或输出的程序。即使是复制文件之类的简单命令也难以在跨平台的方式中执行。例如,在 UNIX 上复制文件使用 cp 命令完成,而在 Windows 上,使用 copy 命令完成。更糟糕的是,在不同的平台上,文件的名称经常会发生变化。在 Windows 上的可执行文件以 .exe 结尾,而在 UNIX 上没有。即使在 UNIX 实现之间,也会有差异,例如共享库使用的扩展名;.so、.sl、.dylib 等。

CMake 提供了三个主要工具来处理这些差异。第一是 -E 选项(用于执行)的缩写 cmake。当可执行 cmake 文件使用 -E 选项时,它充当一个通用的跨平台实用程序命令。 -E 选项之后的参数表明要做什么 cmake。这些选项提供了一种平台无关的方式来执行一些常见任务,包括复制或删除文件、比较和有条件复制、时间、创建符号链接等。cmake 可通过使用 CMakeLists 文件中的变量 CMAKE_COMMAND 引用,稍后的示例将展示这一点。

当然,CMake 不会限制你在所有自定义命令中使用 cmake -E。你可以使用任何你喜欢的命令,尽管执行该操作时考虑可移植性问题非常重要。一个常见做法是使用 find_program 查找可执行文件(例如 Perl),然后在自定义命令中使用该可执行文件。

CMake 提供的用于解决可移植性问题的第二个工具是一些说明平台特性的变量。cmake-variables(7) 手册列出了许多变量,它们对于需要引用具有平台相关名称的文件的自定义指令很有用。这些包括说明文件命名约定的 CMAKE_EXECUTABLE_SUFFIXCMAKE_SHARED_LIBRARY_PREFIX 等。

最后,CMake 在自定义命令中支持 生成器 表达式。这些表达式使用特殊语法 $<...> 并且在处理 CMake 输入文件时不会对其进行评估,而是在生成最终的构建系统之前延迟评估。因此,为其替换的值会知道其评估上下文的全部详细信息,包括当前的构建配置和与目标关联的所有构建属性。

添加自定义命令

现在我们将考虑 add_custom_command 的签名。在 Makefile 术语中,add_custom_command 会将规则添加到 Makefile 中。对于那些更熟悉 Visual Studio 的人而言,它会向文件添加自定义的构建步骤。add_custom_command 有两个主要的签名:一个用于向目标添加自定义命令,另一个用于添加自定义命令以构建文件。

目标是你想向其添加自定义命令的 CMake 目标(可执行文件、库或自定义目标)的名称。你可以选择执行自定义命令的时间。你可以为自定义命令指定任意数量的命令。它们将按指定顺序执行。

现在让我们考虑一个简单的自定义命令,用于在可执行文件构建后对其进行复制。

# first define the executable target as usual
add_executable(Foo bar.c)

# then add the custom command to copy it
add_custom_command(
  TARGET Foo
  POST_BUILD
  COMMAND ${CMAKE_COMMAND}
  ARGS -E copy $<TARGET_FILE:Foo> /testing_department/files
  )

此示例中的第一个命令是通过源文件列表创建一个可执行文件的标准命令。在此情况下,可执行文件名为 Foo,它通过源文件 bar.c 创建。接下来是 add_custom_command 调用。其中,目标仅仅是 Foo,我们正在添加一个后期构建命令。要执行的命令是 cmake,其完整路径在 CMAKE_COMMAND 变量中指定。其参数是 -E copy 以及源和目标位置。在此情况下,它会将 Foo 可执行文件从其构建位置复制到 /testing_department/files 目录。请注意,TARGET 参数接受 CMake 目标(此示例中为 Foo),但指定给 COMMAND 参数的参数通常需要完整路径。在此情况下,我们将 cmake -E copy 传递给可执行文件的完整路径,通过 $<TARGET_FILE:...> 生成器表达式来引用该路径。

生成文件

对于 add_custom_command 的第二个用途是添加有关如何构建输出文件的规则。此处提供的规则将替换任何当前构建该文件的规则。请记住,add_custom_command 产生的输出必须由同一作用域中的目标使用。正如稍后所讨论的那样,add_custom_target 命令可以用于此目的。

使用可执行文件构建源文件

有时,一个软件项目构建一个可执行文件,然后把它用于生成源代码文件,这些文件用于构建其他可执行文件或库。这听起来也许不常见,但这种情况经常出现。一个例子就是 TIFF 库的构建过程,它创建了一个可执行文件,然后运行它来生成一个包含系统特定信息的源代码文件。然后会将此文件用作构建 TIFF 主库的源文件。另一个例子是可视化工具包 (VTK),它构建一个叫做 vtkWrapTcl 的可执行文件,将 C++ 类包装到 Tcl 中。可执行文件在构建后会被用来为构建过程创建更多源代码文件。

###################################################
# Test using a compiled program to create a file
####################################################

# add the executable that will create the file
# build creator executable from creator.cxx
add_executable(creator creator.cxx)

# add the custom command to produce created.c
add_custom_command(
  OUTPUT ${PROJECT_BINARY_DIR}/created.c
  DEPENDS creator
  COMMAND creator ${PROJECT_BINARY_DIR}/created.c
  )

# add an executable that uses created.c
add_executable(Foo ${PROJECT_BINARY_DIR}/created.c)

本例的第一部分根据源代码文件 creator.cxx 生成 creator 可执行文件。然后,自定义命令通过运行可执行文件 creator 来设置生成源代码文件 created.c 的规则。自定义命令依赖于 creator 目标并将它的结果写入输出目录树 (PROJECT_BINARY_DIR)。最后,会添加一个称为 Foo 的可执行目标,它是使用 created.c 源代码文件构建的。CMake 会在 Makefile(或 Visual Studio 工作区)中创建所有必需的规则,以便在你构建项目时,会构建 creator 可执行文件,并运行它以创建 created.c,然后再用它来构建 Foo 可执行文件。

添加自定义目标

到目前为止的讨论中,CMake 目标通常是指可执行文件和库。CMake 支持一个更通用的目标概念,称为自定义目标,可以在你想要目标概念但不想要最终产品是库或可执行文件时使用。自定义目标的示例包括用于构建文档、运行测试或更新网页的目标。要添加一个自定义目标,请使用 add_custom_target 命令。

指定的目标名称将是传递给目标的名称。可以通过 Makefiles (make 名称) 或 Visual Studio(右键单击目标,然后选择生成)来使用该名称专门生成该目标。如果指定了可选的 ALL 自变量,则该目标将包含在 ALL_BUILD 目标中,并且会在生成 Makefile 或项目时自动生成。命令和自变量是可选的;如果指定,它们将作为构建后命令添加到目标中。对于只需要执行命令的自定义目标,这就是您需要的所有内容。更复杂的自定义目标可能依赖于其他文件,在这些情况下,DEPENDS 自变量用于列出该目标所依赖的文件。我们将考虑这两个案例的示例。首先,我们来看一个没有任何依赖关系的自定义目标

add_custom_target(FooJAR ALL
  ${JAR} -cvf "\"${PROJECT_BINARY_DIR}/Foo.jar\""
              "\"${PROJECT_SOURCE_DIR}/Java\""
  )

有了上述定义,每当生成 FooJAR 目标时,它将运行 Java 的归档程序 (jar) 利用 ${PROJECT_SOURCE_DIR}/Java 目录中的 java 类来创建 Foo.jar 文件。从本质上讲,这种类型的自定义目标允许开发人员将命令绑定到目标,以便在构建过程中方便地调用它。现在,我们来看一个更复杂的示例,该示例大致模拟了从 LaTeX 生成 PDF 文件。在这种情况下,自定义目标依赖于其他生成的源文件(主要是最终产品 .pdf 文件)

# Add the rule to build the .dvi file from the .tex
# file. This relies on LATEX being set correctly
#
add_custom_command(
  OUTPUT  ${PROJECT_BINARY_DIR}/doc1.dvi
  DEPENDS ${PROJECT_SOURCE_DIR}/doc1.tex
  COMMAND ${LATEX} ${PROJECT_SOURCE_DIR}/doc1.tex
  )

# Add the rule to produce the .pdf file from the .dvi
# file. This relies on DVIPDF being set correctly
#
add_custom_command(
  OUTPUT  ${PROJECT_BINARY_DIR}/doc1.pdf
  DEPENDS ${PROJECT_BINARY_DIR}/doc1.dvi
  COMMAND ${DVIPDF} ${PROJECT_BINARY_DIR}/doc1.dvi
  )

# finally add the custom target that when invoked
# will cause the generation of the pdf file
#
add_custom_target(TDocument ALL
  DEPENDS ${PROJECT_BINARY_DIR}/doc1.pdf
  )

此示例同时使用了 add_custom_commandadd_custom_target。两个 add_custom_command 调用用于指定从 .tex 文件生成 .pdf 文件的规则。在这种情况下,有两个步骤和两个自定义命令。首先通过运行 LaTeX 从 .tex 文件生成一个 .dvi 文件,然后处理 .dvi 文件以生成所需的 .pdf 文件。最后,添加了一个名为 TDocument 的自定义目标。它的命令仅回显出它正在执行的操作,而实际工作是由两个自定义命令完成的。DEPENDS 自变量设置了自定义目标和自定义命令之间的依赖关系。在生成 TDocument 时,它将首先检查其所有依赖关系是否生成。如果任何关系未生成,它将调用相应的自定义命令来生成它们。此示例可以通过将两个自定义命令合并为一个自定义命令来缩短,如下面的示例所示

# Add the rule to build the .pdf file from the .tex
# file. This relies on LATEX and DVIPDF being set correctly
#
add_custom_command(
  OUTPUT  ${PROJECT_BINARY_DIR}/doc1.pdf
  DEPENDS ${PROJECT_SOURCE_DIR}/doc1.tex
  COMMAND ${LATEX}  ${PROJECT_SOURCE_DIR}/doc1.tex
  COMMAND ${DVIPDF} ${PROJECT_BINARY_DIR}/doc1.dvi
  )

# finally add the custom target that when invoked
# will cause the generation of the pdf file

add_custom_target(TDocument ALL
  DEPENDS ${PROJECT_BINARY_DIR}/doc1.pdf
  )

现在考虑文档由多个文件组成的情况。可以通过使用输入列表和 foreach 循环来修改上述示例以处理多个文件。例如

# set the list of documents to process
set(DOCS doc1 doc2 doc3)

# add the custom commands for each document
foreach(DOC ${DOCS})
  add_custom_command(
    OUTPUT  ${PROJECT_BINARY_DIR}/${DOC}.pdf
    DEPENDS ${PROJECT_SOURCE_DIR}/${DOC}.tex
    COMMAND ${LATEX} ${PROJECT_SOURCE_DIR}/${DOC}.tex
    COMMAND ${DVIPDF} ${PROJECT_BINARY_DIR}/${DOC}.dvi
    )

  # build a list of all the results
  list(APPEND DOC_RESULTS ${PROJECT_BINARY_DIR}/${DOC}.pdf)
endforeach()

# finally add the custom target that when invoked
# will cause the generation of the pdf file
add_custom_target(TDocument ALL
  DEPENDS ${DOC_RESULTS}
  )

在此示例中,生成自定义目标 TDocument 将导致生成所有指定的 .pdf 文件。向列表中添加新文档只需将其文件名添加到示例顶部的 DOCS 变量即可。

指定依赖关系和输出

在使用自定命令和自定义目标时,通常会指定依赖项。在指定依赖项或自定命令的输出时,应始终指定完整路径。例如,如果命令在二进制树中生成 foo.h,则其输出应类似于 ${PROJECT_BINARY_DIR}/foo.h。如果未指定文件的正确路径,CMake 将尝试确定该路径;复杂的项目通常最终在源树和构建树中同时使用文件,如果未指定完整路径,最终会导致错误。

将目标指定为依赖项时,您可以省略完整路径和可执行扩展名,只需按名称引用即可。考虑本章节前面示例中将生成器目标指定为 add_custom_command 依赖项。CMake 识别 creator 与现有目标匹配,并正确处理依赖项。

一个规则无法对应一个输出时

在使用需要做进一步说明的自定命令时,会出现一些不寻常的情况。第一种情况是一个命令(或可执行文件)可以创建多个输出,第二种情况是多个命令可用于创建单个输出。

单个命令生成多个输出

在 CMake 中,自定命令只需在 OUTPUT 关键字后面列出多个输出,即可生成多个输出。CMake 将为构建系统创建正确的规则,以便无论目标需要哪个输出,都将运行正确的规则。如果可执行文件碰巧生成了几个输出,但构建过程仅使用其中的一个,那么您在创建自定命令时只需忽略其他输出即可。假设可执行文件生成了构建过程中使用到的源文件,还生成了一个未使用的执行日志。自定命令应将源文件指定为输出,而忽略还生成了日志文件这一事实。

具有多个输出的单个命令的另一种情况是,当命令相同,但其参数发生改变时。这实际上等同于具有不同的命令,并且每种情况应具有自己的自定命令。文档示例就是一个例子,其中为每个 .tex 文件添加了一个自定命令。该命令相同,但每次传递给它的参数都会发生改变。

可以使用不同的命令生成一个输出

在罕见的情况下,你可能会发现有多个可用于生成输出的命令。大多数构建系统(例如 make 和 Visual Studio)不支持这种情况,而 CMake 也不支持。有两个常见的方法可用于解决此问题。如果你确实有两个不同的命令产生相同的输出且没有其他重要输出,那么你可以简单地选择其中一个并为它创建一个自定义命令。

在更复杂的情况下,有多个命令具有多个输出;例如

Command1 produces foo.h and bar.h
Command2 produces widget.h and bar.h

在这种情况下,可以使用几种方法。你可以考虑将两个命令和三个输出都组合到一个自定义命令中,以便每当需要一个输出时,这三个输出都将同时构建。你还可以创建三个自定义命令,每个命令对应一个唯一输出。用于 foo.h 的自定义命令将调用命令 1,而用于 widget.h 的自定义命令将调用命令 2。在为 bar.h 指定自定义命令时,你可以选择命令 1 或命令 2。