步骤 7:自定义命令和生成文件

代码生成是一种无处不在的机制,用于将编程语言扩展到其语言模型之外。CMake 对 Qt 的元对象编译器 (Meta-Object Compiler) 提供了一流的支持,但很少有其他代码生成器足够重要,值得为此付出如此大的努力。

相反,代码生成器倾向于定制化和特定用途。CMake 提供了描述代码生成器用法的工具,以便项目可以添加对其个体需求的支持。

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

背景

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

注意

此模型假定过程的输出在运行前是已知的。CMake 缺乏描述代码生成器的能力,其中输出的名称和位置取决于输入的*内容*。存在各种技巧可以将此功能集成到 CMake 中,但它们超出了本教程的范围。

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

对于源文件,这与将生成的文件添加到 STATICSHAREDOBJECT 库的源列表一样简单。对于仅标头 (header-only) 生成器,通常需要使用通过 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: Click to show/hide answer
TODO 1-2: MathFunctions/MakeTable/CMakeLists.txt
add_executable(MakeTable)

target_sources(MakeTable
  PRIVATE
    MakeTable.cxx
)

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

TODO 3-4: Click to show/hide answer
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: Click to show/hide answer
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: Click to show/hide answer
TODO 10: MathFunctions/MathFunctions.cxx
#include <SqrtTable.h>

double table_sqrt(double x)
{