本页面简要介绍了 Ninja 在 紫红色。
总体概述
Fuchsia 构建系统使用 Ninja 并行启动构建命令。 以下步骤介绍了 Ninja 的行为:
从顶级
build.ninja
加载 Ninja 构建计划 该文件自身可以包含几个其他.ninja
文件。在 Fuchsia build 中,这些 build 由 GN build 创建 工具。此操作会在内存中构建依赖关系图。
加载 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
有关详情,请参阅 Fuchsia 功能:待处理命令的状态。
Ninja build 依赖关系图
Ninja 根据构建计划构建的图表仅包含两种类型 节点3:
目标节点:一个目标节点直接对应一个已知的文件路径。 Ninja。该路径始终相对于 build 目录。
操作节点:操作节点模拟要运行的单个命令 根据一组给定的输入文件生成输出文件。
请注意以下信息:
不是任何 Action 节点输出的目标节点称为源 文件。
不是任何 Action 节点的输入的目标节点必须是输出 称为最终输出。
一个目标节点,它既是操作的输出,又是输入 称为“中间目标”或“中间输出”
每个操作可以指向图表中的零个或多个输入目标节点。
每个 Action 在图表中可以有一个或多个输出“目标”节点。 一个操作不能有零输出,否则 Ninja 不知道 运行其命令。
操作节点没有名称,因此无法直接引用它们 。仅限文件路径,即目标。
Ninja 构建计划
Ninja 构建计划由 build.ninja
文件(位于
build 目录,该目录可包含其他*.ninja
文件include
或
subninja
语句。下面总结了其中最重要的部分
功能(如需了解完整详情,请参阅 Ninja 手册)。
在 .ninja
文件中,通过 build
语句定义操作节点:
build <outputs>: <rule_name> <inputs>
<outputs>
是输出路径列表,<inputs>
是输入路径列表。
<rule_name>
是忍者规则的名称,它充当食谱
来编写要运行的最终命令规则由特殊的 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 要构建
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
文件。
隐式输入
同样,也可以告诉 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()
加载。
通过 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 调用时,将使用构建日志时间戳,而不是
文件系统一(如果较新)来确定是否需要对文件
重新生成。因此,在上面的示例中,
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>
是 build 目录,<target>
是输出文件的路径, 至<build_dir>
。-t deps
选项会调用 Ninja 工具, 此输出文件的依赖项日志的内容。但请注意,依赖项日志是一个仅用于附加的二进制文件,因此 在多个 Ninja 构建调用中累积
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 强制执行。
关于 depfile 的正确性问题
在构建计划没有变化时,依赖项日志会非常好用
因为 Ninja 会在 depfile
列出的下一个增量 build 上检测到它
发生变化的隐式输入,并重新构建依赖于这些输入的任何内容。
但是,当构建计划发生更改时,Ninja 依赖项日志中的条目 ,并会在 下一次 Ninja 调用。有时,这些会破坏下个构建 调用。特别是在将依赖项从 但仍会记录在依赖项日志中。
在实践中,这会导致随机发生的增量构建失败 还是在我们的基础架构构建器上(无论是 CQ 还是 CI)。遗憾的是 解决这一问题的方法,因为依赖项日志是 Ninja 的 并且不可能检测到“过时”(因为确切的 旧版构建计划在使用时早已不复存在了)。
通常的权宜解决方法是执行干净构建。Fuchsia 甚至 “clean build fences”解决最严重的问题 问题案例。
-
更具体地说,Ninja 依赖关系图与 Bazel 操作图非常相似,并且 Ninja 目标对应于 Bazel 文件对象。↩
-
在构建期间,如果 Ninja 确定某些命令的输出被视为最新,此数字可能会降低。↩
-
由于历史原因,Ninja 源代码使用名为
Node
的 C++ 类对目标建模,并使用名为Edge
的 C++ 类对操作进行建模。不过,由于在阅读代码时经常会遇到非常 困惑的问题,因此本文档不会遵循这种误导性惯例。↩