本页面简要介绍了 Ninja 在紫红色中的工作原理。
总体概述
Fuchsia 构建系统使用 Ninja 并行启动构建命令。以下步骤描述了 Ninja 的行为:
从顶级
build.ninja
文件(该文件本身可以包含其他几个.ninja
文件)中加载 Ninja 构建计划。加载 Ninja 构建日志和依赖项日志(如果存在)。
此操作会添加上一次成功的 Ninja build 调用期间发现的依赖项边缘。这样可以实现快速的增量构建,但会牺牲一定程度的正确性。
确定需要生成哪些构建输出(也称为“目标”)。
从命令行中指定的目标开始,以递归方式遍历其依赖项,以确定哪些最终输出和中间输出相对于其输入过时,因此需要重新构建。需要重新运行的命令会在有向无环图中正确排序。
根据主机系统上的 CPU 数量(或显式
-j<count>
参数)并行启动所需的构建命令。控制并行性的另一种方法是使用
-l<max_load>
限制系统上的最大负载值。ninja
知道的输入更新后,命令就可运行。
状态显示
在构建期间,Ninja 会并行启动多个命令,并且默认情况下,它会缓冲其输出(stdout 和 stderr),直到命令完成为止。
Ninja 还会输出描述以下内容的状态行(例如,使用 fx build
时):
- 已完成的命令的数量。
- 完成构建必须运行的命令总数2
- 当前正在运行的命令的数量。
- last-completed 命令的说明。这通常包括少量助记符(例如
ACTION
或CXX
),后跟一个输出目标列表。
[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
文件定义,该文件可以包含带有 include
或 subninja
语句的其他 *.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 甚至实现了“干净的构建栅栏”来应对最有问题的情况。
-
具体而言,Ninja 依赖关系图与 Bazel 操作图非常相似,且 Ninja 目标对应于 Bazel File 对象。↩
-
如果 Ninja 确定某些命令的输出被视为最新,此数字可能会在构建期间减少。↩
-
由于历史原因,Ninja 源代码使用名为
Node
的 C++ 类对目标进行建模,并使用名为Edge
的 C++ 类对操作建模。不过,由于在阅读代码时经常会造成混淆,因此本文不会遵循这一误导性惯例。↩