本页面简要介绍了 Ninja 在 Fuchsia 中的工作方式。
注意:大多数开发者应通过
fx工作流(例如fx build和fx set)与构建系统互动,而不是直接运行 GN、Ninja 或 Bazel(即使是通过fx gn或fx ninja等封装容器)。
一般概览
Fuchsia 构建系统使用 Ninja 并行启动构建命令。以下步骤描述了 Ninja 的行为:
从顶级
build.ninja文件加载 Ninja build 计划,该文件本身可以包含多个其他.ninja文件。加载 Ninja 构建日志和依赖项日志(如果存在)。
此操作会添加在上一次成功的 Ninja 构建调用期间发现的依赖关系边。这样可以实现快速增量构建,但正确性会受到一定影响。
确定需要生成哪些 build 输出(也称为“目标”)。
从命令行中指定的目标开始,以递归方式遍历其依赖项,以确定哪些最终输出和中间输出相对于其输入而言已过时,因此需要重新构建。需要重新运行的命令在有向无环图中正确排序。
根据主机系统上的 CPU 数量(或显式
-j<count>参数)并行启动所需的 build 命令。控制并行性的另一种方法是使用
-l<max_load>限制系统的最大负载值。当ninja知道的输入已更新时,命令即可运行。
状态显示
在构建期间,Ninja 会并行启动多个命令,并且默认情况下,它会缓冲这些命令的输出(stdout 和 stderr),直到它们完成。
Ninja 还会打印一个状态行(例如,当使用 fx build 时),其中描述了以下内容:
- 已完成的命令数。
- 完成 build 必须运行的命令总数2
- 当前正在运行的命令数。
- 对上次完成的命令的说明。通常包括一个简短的助记符(例如
ACTION或CXX),后跟输出目标列表。
[102/345](36) ACTION path/to/some/build/artifact
上述示例表示,在 345 个命令中,目前已完成 102 个命令,Ninja 当前启动了 36 个并行命令,而 path/to/some/build/artifact 是最新生成的 build 制品。
您可以设置 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 将显示一个包含最旧的长时间运行命令及其运行时间的表格,以便更好地了解 build 期间发生的情况。此功能仅在智能终端中启用。
在环境中设置 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
如需了解详情,请参阅 Fuchsia 功能:待处理命令的状态。
Ninja build 依赖关系图
Ninja 根据 build 计划构建的图仅包含两种类型的节点3:
目标节点:目标节点只是与 Ninja 已知的文件路径相对应。该路径始终相对于 build 目录。
操作节点:操作节点用于对单个命令进行建模,该命令用于根据给定的输入文件集生成输出文件。
请注意以下信息:
如果某个 Target 节点不是任何 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,这些关键字将展开为相应 build 规则的输入和输出列表。
rule copy_file
command = cp -f $in $out
build output.txt: copy_file input.txt
上面的示例是一个简单的 build 计划,它告诉 Ninja,如需构建 output.txt,必须运行命令 cp -f input.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,但顺序并不重要。换句话说,可以在生成 foo.so 的命令之前运行生成 program 的命令。在此示例中,如果二进制文件仅在运行时通过 dlopen() 加载库,则此方法有效。
通过 restat 优化减少重建次数
如果某些命令的内容未发生变化,它们可能不会更改输出文件的时间戳。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_list.txt 的更改不会改变 package_manifest.json 输出文件,则无需重新生成最终的 package_archive.zip。为了支持这一点,Ninja 每次运行命令时,都会将每个输出文件的摘要(包括命令的哈希和最新输入的时间戳)记录到 $BUILD_DIR/.ninja_build 中的一个特殊文件(称为 Ninja 构建日志)中。
在下一次调用 Ninja 时,系统会使用 build 日志时间戳(如果该时间戳较新)而不是文件系统时间戳来确定是否需要重新生成文件。因此,在上述示例中,即使 package_list.txt 的文件系统时间戳较旧,较新的时间戳也会与 package_manifest.json 相关联。如果没有此功能,Ninja 会尝试在每次调用 build 时重新构建清单文件。
使用 depfile 在 build 时发现隐式输入
由 Ninja 启动的命令可以生成一个特殊的依赖项文件(简称为 depfile),其中列出了额外的 隐式 输入,即命令的输入(但这些输入不会出现在 build 计划中)。此信息由 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>是 build 目录,<target>是相对于<build_dir>的输出文件路径。-t deps选项会调用一个 Ninja 工具,该工具会打印相应输出文件的依赖项日志的内容。不过请注意,deps 日志是一个仅可附加的二进制文件,因此会在多次调用 Ninja 构建时累积
depfile依赖项,因此该日志可能会列出比上次命令生成的隐式依赖项更多的隐式依赖项。移除 build 工件,然后使用
-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 不会强制执行此操作。
depfile 的正确性问题
如果构建计划没有变化,依赖项日志的效果非常好,因为 Ninja 会在下一次增量构建时检测到哪些 depfile 列出的隐式输入发生了变化,并重新构建依赖于这些输入的所有内容。
不过,当 build 计划发生更改时,Ninja 依赖项日志中的条目可能会变得过时,并在下一次调用 Ninja 时在依赖项图中添加错误的边。有时,这些错误会破坏下一次 build 调用。当从 build 计划中移除依赖项但仍记录在依赖项日志中时,尤其会发生这种情况。
在实践中,这会导致随机的增量 build 失败,这些失败可能发生在本地,也可能发生在我们的基础架构 build 服务器上(无论是在 CQ 中还是在 CI 中)。遗憾的是,目前无法解决此问题,因为依赖项日志是 Ninja 设计的一部分,并且无法检测“过时”的条目(因为旧 build 计划的具体详细信息在被使用时早已消失)。
通常的解决方法是执行干净 build。Fuchsia 甚至实现了“干净构建栅栏”来解决最棘手的问题。
-
更具体地说,Ninja 依赖关系图与 Bazel 操作图非常相似,并且 Ninja 目标对应于 Bazel File 对象。↩
-
如果 Ninja 确定某些命令的输出被视为最新,则此数字在 build 期间可能会减少。↩
-
出于历史原因,Ninja 源代码使用名为
Node的 C++ 类来对目标进行建模,并使用名为Edge的 C++ 类来对操作进行建模。不过,由于在阅读代码时,这种做法经常会造成很大的困惑,因此本文档将不会遵循这种误导性的惯例。 ↩