第七步:自定义命令与生成文件

代码生成是一种通用机制,用于将编程语言扩展到其语言模型限制之外。CMake 对 Qt 的元对象编译器(Meta-Object Compiler)有顶级支持,但很少有其他代码生成器值得投入这种程度的精力。

相反,代码生成器通常是定制的且针对特定用途。CMake 提供了描述代码生成器用法的工具,因此项目可以根据其个人需求添加支持。

在这一步中,我们将使用 add_custom_command() 在教程项目中添加对代码生成器的支持。

背景

构建过程中的任何步骤通常都可以根据其输入和输出进行描述。CMake 假设代码生成器和其他自定义进程遵循相同的原则。通过这种方式,代码生成器与编译器、链接器和工具链的其他元素的作用完全相同;当输入比输出更新时(或者输出不存在),将运行用户指定的命令来更新输出。

注意

此模型假设进程的输出在运行前是已知的。CMake 无法描述输出的名称和位置取决于输入内容的代码生成器。虽然存在各种黑客手段将此功能引入 CMake,但它们超出了本教程的范围。

描述代码生成器(或任何自定义进程)通常分为两部分。首先,输入和输出的描述独立于 CMake 目标模型,仅关注生成过程本身。其次,将输出与 CMake 目标关联起来,以将其插入到 CMake 目标模型中。

对于源文件,这就像将生成的文件添加到 STATICSHAREDOBJECT 库的源列表中一样简单。对于仅头文件的生成器,通常需要使用通过 add_custom_target() 创建的中间目标,将头文件生成添加到构建阶段(因为 INTERFACE 库没有构建步骤)。

练习 1 - 使用代码生成器

描述代码生成器的主要机制是 add_custom_command() 命令。就 add_custom_command() 而言,“命令”要么是构建环境中可用的可执行文件,要么是 CMake 可执行目标名称。

add_executable(Tool)
# ...
add_custom_command(
  OUTPUT Generated.cxx
  COMMAND Tool -i input.txt -o Generated.cxx
  DEPENDS Tool input.txt
  VERBATIM
)
# ...
add_library(GeneratedObject OBJECT)
target_sources(GeneratedObject
  PRIVATE
    Generated.cxx
)

除了 VERBATIM 之外,大多数关键字都是不言自明的。由于在现代语境下解释起来毫无意义的历史原因,此参数实际上是强制性的。好奇的读者可以查阅 add_custom_command() 文档以获取更多详细信息。

Tool 可执行目标同时出现在 COMMANDDEPENDS 参数中。虽然 COMMAND 足以让代码正确构建,但将 Tool 本身作为自定义命令的依赖项,可以确保如果 Tool 更新,自定义命令将重新运行。

对于仅头文件的生成,需要额外的命令,因为库本身没有构建步骤。我们可以使用 add_custom_target() 为库创建一个“人工”构建步骤。然后,我们使用 add_dependencies() 命令强制自定义目标在任何链接该库的目标之前运行。

add_custom_target(RunGenerator DEPENDS Generated.h)

add_library(GeneratedLib INTERFACE)
target_sources(GeneratedLib
  INTERFACE
    FILE_SET HEADERS
    BASE_DIRS
      ${CMAKE_CURRENT_BINARY_DIR}
    FILES
      ${CMAKE_CURRENT_BINARY_DIR}/Generated.h
)

add_dependencies(GeneratedLib RunGenerator)

注意

我们将 CMAKE_CURRENT_BINARY_DIR(一个命名我们的构件所放置的构建树中当前位置的变量)添加到基础目录中,因为这是我们将要运行代码生成器的工作目录。出于构建目的,列出 FILES 是不必要的,此处列出仅为了清晰起见。

目标

将预先计算的平方根生成表添加到 MathFunctions 库中。

有用资源

要编辑的文件

  • MathFunctions/CMakeLists.txt

  • MathFunctions/MakeTable/CMakeLists.txt

  • MathFunctions/MathFunctions.cxx

开始

MathFunctions 库已被修改,以便在给定小于 10 的数字时使用预先计算的表。然而,硬编码的表并不十分准确,仅包含最接近的截断整数值。

MakeTable.cxx 源文件描述了一个将生成更好表的程序。它以单个参数作为输入,即要生成的表的文件名。

完成 TODO 1TODO 10

构建并运行

无需特殊配置,像往常一样配置和构建即可。注意 MakeTable 可执行文件在 MathFunctions 之前进行序列化构建。

cmake --preset tutorial
cmake --build build

验证 Tutorial 的输出现在是否对小于 10 的值使用了预先计算的表。

解决方案

首先,我们添加一个新的可执行文件来生成表,并将 MakeTable.cxx 文件作为源文件添加。

TODO 1-2:点击显示/隐藏答案
TODO 1-2:MathFunctions/MakeTable/CMakeLists.txt
add_executable(MakeTable)

target_sources(MakeTable
  PRIVATE
    MakeTable.cxx
)

然后我们添加一个生成该表的自定义命令,以及一个依赖于该表的自定义目标。

TODO 3-4:点击显示/隐藏答案
TODO 3-4:MathFunctions/MakeTable/CMakeLists.txt
add_custom_command(
  OUTPUT SqrtTable.h
  COMMAND MakeTable SqrtTable.h
  DEPENDS MakeTable
  VERBATIM
)

add_custom_target(RunMakeTable DEPENDS SqrtTable.h)

我们需要添加一个接口库,描述将出现在 CMAKE_CURRENT_BINARY_DIR 中的输出。FILES 参数是可选的。

TODO 5-6:点击显示/隐藏答案
TODO 5-6:MathFunctions/MakeTable/CMakeLists.txt
add_library(SqrtTable INTERFACE)

target_sources(SqrtTable
  INTERFACE
    FILE_SET HEADERS
    BASE_DIRS
      ${CMAKE_CURRENT_BINARY_DIR}
    FILES
      ${CMAKE_CURRENT_BINARY_DIR}/SqrtTable.h
)

现在所有目标都已描述完毕,我们可以通过将它们与 add_dependencies() 关联,强制自定义目标在接口库的任何依赖项之前运行。

TODO 7:点击显示/隐藏答案
TODO 7:MathFunctions/MakeTable/CMakeLists.txt
add_dependencies(SqrtTable RunMakeTable)

我们准备好将接口库添加到 MathFunctions 的链接库中,并将整个 MakeTable 文件夹添加到项目中。

TODO 8-9: 点击显示/隐藏答案
TODO 9:MathFunctions/CMakeLists.txt
add_subdirectory(MakeTable)

最后,我们更新 MathFunctions 库本身,以利用生成的表。

TODO 10:点击显示/隐藏答案
TODO 10:MathFunctions/MathFunctions.cxx
#include <SqrtTable.h>

double table_sqrt(double x)
{