忍者游戏

本页面简要介绍了 Ninja 在紫红色中的工作原理。

总体概述

Fuchsia 构建系统使用 Ninja 并行启动构建命令。以下步骤描述了 Ninja 的行为:

  1. 从顶级 build.ninja 文件(该文件本身可以包含其他几个 .ninja 文件)中加载 Ninja 构建计划

    在 Fuchsia build 中,这些映像由 GN 构建工具创建。此操作会在内存中构建依赖关系图

  2. 加载 Ninja 构建日志和依赖项日志(如果存在)。

    此操作会添加上一次成功的 Ninja build 调用期间发现的依赖项边缘。这样可以实现快速的增量构建,但会牺牲一定程度的正确性

  3. 确定需要生成哪些构建输出(也称为“目标”)。

    从命令行中指定的目标开始,以递归方式遍历其依赖项,以确定哪些最终输出和中间输出相对于其输入过时,因此需要重新构建。需要重新运行的命令会在有向无环图中正确排序。

  4. 根据主机系统上的 CPU 数量(或显式 -j<count> 参数)并行启动所需的构建命令。

    控制并行性的另一种方法是使用 -l<max_load> 限制系统上的最大负载值。ninja 知道的输入更新后,命令就可运行。

状态显示

在构建期间,Ninja 会并行启动多个命令,并且默认情况下,它会缓冲其输出(stdout 和 stderr),直到命令完成为止。

Ninja 还会输出描述以下内容的状态行(例如,使用 fx build 时):

  • 已完成的命令的数量。
  • 完成构建必须运行的命令总数2
  • 当前正在运行的命令的数量。
  • last-completed 命令的说明。这通常包括少量助记符(例如 ACTIONCXX),后跟一个输出目标列表。
[102/345](36) ACTION path/to/some/build/artifact

上面的示例意味着,目前已完成 102 个命令(共 345 个),Ninja 当前启动了 36 个并行命令,而 path/to/some/build/artifact 是要生成的最新构建工件。

您可以通过设置 NINJA_STATUS 环境变量来自定义状态行的内容。

如果任何命令生成了一些输出,或者失败了,Ninja 将使用该命令的说明更新状态行,然后输出其输出或错误消息。然后,它会继续输出其后面的状态行,例如:

[102/345](24) ACTION path/to/some/build/artifact
<output of the command which generated 'path/to/some/build/artifact'>
[101/345](23) ACTION path/to/another/build/artifact

实际上,大多数编译器警告就是这样输出的。

一种特殊的例外情况是,如果命令位于特殊的 console中,则它能够直接输出到终端。这对于需要输出自己的状态更新的长时间运行的命令非常有用。

Ninja 可确保一次只能启动一个控制台命令,并会暂停自己的状态行更新,直到其完成为止。但请注意,其他非控制台命令仍会在后台并行运行,并且其输出会被缓冲。Fuchsia build 将此功能用于调用 Bazel 的所有命令,因为这些命令往往很长,而 Bazel 会自行向终端提供状态更新。

Fuchsia 特定状态显示

作为一项特定于 Fuchsia 的特殊改进,Ninja 将显示一个表格,其中包含最旧的长时间运行的命令及其运行时间,以便更好地了解构建期间发生的情况。此功能仅在智能终端中启用。

在您的环境中设置 NINJA_STATUS_MAX_COMMANDS=<count> 可更改显示的命令数量。fx build 将其默认值设为 4,如下所示:

[0/28477](260) STAMP host_x64/obj/tools/configc/configc_sdk_meta_generated_file.stamp
  0.4s | STAMP obj/sdk/zircon_sysroot_meta_verify.stamp
  0.4s | CXX obj/BUILD_DIR/fidling/gen/sdk/fidl/fuchsia.me...chsia.media/cpp/fuchsia.media_cpp_common.common_types.cc.o
  0.4s | CXX obj/BUILD_DIR/fidling/gen/sdk/fidl/fuchsia.me...fuchsia.media/cpp/fuchsia.media_cpp.natural_messaging.cc.o
  0.4s | CXX obj/BUILD_DIR/fidling/gen/sdk/fidl/fuchsia.me...dia/cpp/fuchsia.media_cpp_natural_types.natural_types.cc.o

如需了解详情,请参阅紫红色功能:待处理命令的状态

Ninja build 依赖关系图

Ninja 根据构建计划构建的图仅包含两种类型的节点3

  • Target 节点:Target 节点仅对应于 Ninja 已知的文件路径。该路径始终相对于 build 目录。

  • 操作节点:操作节点会对要运行的单个命令建模,以便根据一组给定的输入文件生成输出文件。

请注意以下信息:

  • 不是任何 Action 节点输出的目标节点称为源文件。

  • 如果某个目标节点不是任何 Action 节点的输入,该节点必须是给定 Action 节点的输出,这种节点称为最终输出。

  • 如果某个目标节点既是 Action 的输出,又是另一个 Action 的输出,那么称为中间目标或中间输出。

  • 每个 Action 都可以指向图表中的零个或多个输入 Target 节点。

  • 每个 Action 在图中都可以有一个或多个输出 Target 节点。一项操作不能有零输出,否则 Ninja 不知道何时运行其命令。

  • Action 节点没有名称,因此在调用 Ninja 时无法直接引用它们。仅文件路径,即目标。

Ninja 构建计划

Ninja 构建计划由构建目录顶部的 build.ninja 文件定义,该文件可以包含带有 includesubninja 语句的其他 *.ninja 文件。下面总结了其中最重要的功能(如需了解完整详情,请参阅 Ninja 手册)。

.ninja 文件中,操作节点通过 build 语句定义:

build <outputs>: <rule_name> <inputs>

<outputs> 是输出路径列表,<inputs> 是输入路径列表,<rule_name> 是 Ninja 规则的名称,后者充当编写要运行的最终命令的配方规则由特殊的 rule 语句定义:

rule <rule_name>
   command = <command expression>

<command expression> 可以包含特殊的 $in$out 关键字,这些关键字将扩展到相应构建规则的输入和输出列表中。

rule copy_file
  command = cp -f $in $out

build output.txt: copy_file input.txt

上面的示例是一个非常重要的构建计划,它告知 Ninja 必须运行 cp -f input.txt output.txt 命令才能构建 output.txt

隐式输出

命令中可能会有 $out 扩展项中不得出现的额外输出项。可以使用 | 分隔符将输出分隔到显式输出中。

rule copy_file
  command = cp -f $in $out && touch $out.stamp

build output.txt | output.txt.stamp: copy_file input.txt

上面的示例会告知 Ninja,用于构建 output.txt 的命令会将 input.txt 复制到其中并创建 output.txt.stamp 文件。

隐式输入

同样,也可以使用 build 语句右侧的 | 来告知 Ninja 不应从 $in 表达式扩展某些输入。

rule cxx_compile
  command = c++ -c $in -o $out

build foo.o: cxx_compile foo.cc | foo.h

上面的示例会告知 Ninja,编译 foo.cc 会将 foo.h 用作输入,即使该文件未明确出现在编译器命令中也是如此。

纯订单输入

您可以告知 Ninja,某些文件路径是某些输出的运行时依赖项,因此应该“使用”它们进行构建。它使用 build 语句右侧的 || 分隔符,并且必须始终显示在任何可能的 | 分隔符(如果有)之后。

rule cxx_binary
  command = c++ -o $out $in -ldl

rule cxx_shared_library
  command = c++ -shared -o $out $in

build foo.so: cxx_shared_library:

build program: cxx_binary main.cc || libfoo.so

上面的示例会告知 Ninja,每当需要构建 program 时,也需要构建 foo.so,但这个顺序并不重要。换句话说,可以先运行生成 program 的命令,然后再运行生成 foo.so 的命令。在此示例中,如果二进制文件在运行时仅通过 dlopen() 加载库,此方法即可正常运行。

通过重组优化减少重建

如果某些命令的内容未更改,则其输出文件的时间戳可能不会更改。Ninja 可以使用此功能减少在构建调用期间运行的命令总数。

为了支持这一点,规则定义必须将特殊的 restat 变量设置为非空值。这会导致 Ninja 在执行命令后重新统计其输出。修改时间未更改的每个输出都将被视为从未需要构建,Ninja 会从待处理命令列表中移除将其用作输入的所有命令。

# A rule to invoke the create_manifest.py script that processes some input
# and generates a manifest as output. `restat` is set to indicate that the
# script will not update $out's timestamp if the file exists and its content
# is already correct.

rule create_manifest
  command = ../../create_manifest.py --input $in --output $out
  restat = 1

build package_manifest.json: create_manifest package_list.txt

build package_archive.zip: create_archive package_manifest.json

在上面的示例中,如果开发者以不会更改 package_manifest.json 输出文件的方式更改 package_list.txt,则无需重新生成最终的 package_archive.zip。为此,Ninja 每次运行命令时,都会为每个输出文件记录一份摘要,其中包含命令的哈希值和最近输入的时间戳,该文件位于 $BUILD_DIR/.ninja_build 的一个特殊文件中,该文件称为 Ninja 构建日志

在下一次调用 Ninja 时,系统会使用构建日志时间戳而不是文件系统时间戳(如果是较新的时间戳),来确定是否需要重新生成该文件。因此,在上面的示例中,package_list.txt 的较新时间戳将与 package_manifest.json 相关联,即使其文件系统时间戳较旧也是如此。如果没有此功能,Ninja 将尝试在每次构建调用时重新构建清单文件。

使用 depfile 在构建时发现隐式输入

Ninja 启动的命令可以生成一个特殊的依赖项文件(缩写为 depfile),其中会列出额外的隐式输入(即,未出现在构建计划中的命令的输入)。Ninja 会读取这些信息,并将其记录在名为 $BUILD_DIR/.ninja_deps 的二进制文件(称为“Ninja 依赖项日志”)中。在下一次 Ninja 调用时,依赖项日志会自动加载,并且记录的所有隐式输入都会添加到依赖关系图中。

例如,这对于通过 C++ 编译命令列出所有包含的头文件(甚至是未在相应 .ninja 文件中明确列出的头文件)而言很有用。如果开发者修改了此类头文件,下次 Ninja 调用便会看到相应更改,并导致相应的 C++ 源文件及其所有依赖项被重新编译。

其工作原理是,向规则定义添加 depfile 变量声明,如下所示:

rule cc
    depfile = $out.d
    command = gcc -MD -MF $out.d [other gcc flags here]

请注意,默认情况下,depfile 在被提取到二进制文件依赖项日志后会被 Ninja 移除。如需检查记录了哪些 depfile 依赖项,请执行以下操作之一:

  • 运行 ninja -C <build_dir> -t deps <target>,其中 <build_dir> 是构建目录,<target> 是输出文件的路径(相对于 <build_dir>)。-t deps 选项会调用 Ninja 工具,以便输出此输出文件的依赖项日志内容。

    但请注意,deps 日志是仅支持附加的二进制文件,因此会在多次 Ninja build 调用中累积 depfile 依赖项,因此与最后一个命令生成的依赖项相比,此列表可能会列出更多的隐式依赖项。

  • 移除构建工件,然后使用 -d keepdepfile 选项调用 ninja,这会强制 Ninja 将所有依赖项文件保留在 build 目录中(在将其内容复制到二进制文件依赖项日志之后)。这样即可手动检查其内容,例如:

    $ rm $BUILD_DIR/foo.o
    $ ninja -C $BUILD_DIR -d keepdepfile foo.o
    $ cat $BUILD_DIR/foo.o.d
    

    请注意,确切的 depfile 路径取决于规则定义。按照惯例,大多数命令只会将 .d 后缀附加到第一个输出路径,但 Ninja 不会强制执行此操作。

依赖项问题

当构建计划没有更改时,deps 日志的效果非常好,因为 Ninja 将在下一个增量构建时检测出 depfile 列出的隐式输入在哪里发生更改,并重新构建任何依赖于这些输入的内容。

但是,当构建计划发生更改时,Ninja 依赖项日志中的条目可能会变得过时,并且会在下一次 Ninja 调用的依赖关系图中添加不正确的边缘。有时,这些命令会破坏下一次构建调用。尤其是从构建计划中移除依赖项,但仍记录在依赖项日志中时。

实际上,这会导致随机的增量构建失败,可能在本地或我们的基础架构构建器上(在 CQ 或 CI 中)发生。遗憾的是,目前有办法解决这个问题,因为 deps 日志是 Ninja 的设计的一部分,无法检测到“过时”的条目(因为旧构建计划的确切细节在使用它们时已不复存在)。

通常,解决方法是执行干净 build。Fuchsia 甚至实现了“干净的构建栅栏”来应对最有问题的情况。


  1. 具体而言,Ninja 依赖关系图与 Bazel 操作图非常相似,且 Ninja 目标对应于 Bazel File 对象。

  2. 如果 Ninja 确定某些命令的输出被视为最新,此数字可能会在构建期间减少。

  3. 由于历史原因,Ninja 源代码使用名为 Node 的 C++ 类对目标进行建模,并使用名为 Edge 的 C++ 类对操作建模。不过,由于在阅读代码时经常会造成混淆,因此本文不会遵循这一误导性惯例。