阅读 446

Ninja 构建系统-03

注:本文来自网络资料整理,主要是汇总学习资料,方便完成FlutterEngine编译提供一个完整的学习资料路线

概述

Ninja 是一个构建系统,与 Make 类似。作为输入,你需要描述将源文件处理为目标文件这一过程所需的命令。 Ninja 使用这些命令保持目标处于最新状态。与其它一些构建系统不同,Ninja 的主要设计目标是速度。 Ninja 也有其它一些设计目标:Ninja 必须易于嵌入大型构建系统。 Ninja 获得了相当的成功,逐渐取代了所使用的构建系统。Ninja 公开后,一些人贡献了代码,使得流行的 CMake 构建系统能够生成 Ninja 文件。现在,Ninja 也被用来开发基于 CMake 的系统,如 LLVM 和 ReactOS。其它一些拥有定制构建系统的项目,如 TextMate,直接将 Ninja 作为其构建目标。

历史小介

Ninja 的设计

在高层视角下,任何构建系统主要执行三项任务。

  • (1)加载和分析构建目标,
  • (2)计算出达到构建目标所需的步骤,
  • (3)执行这些步骤。

Ninja 做了什么

Ninja

将绝大部分工作推给了构建文件生成器,那自己还有什么事呢?上述想法理论看上很不错,但真实世界的需要永远更复杂。Ninja 在开发过程中添加(也失去)了很多特性。不论何时,重要的问题都是“我们能做得更少吗?”此处概述这如何运作。 在构建规则出错时需要人去调试(构建)文件,所以 .ninja 构建文件是普通文本,与 Makefiles 类似。为了增强可读性.ninja 也支持一些抽象。

第一种抽象是“rule”

可以代表单个命令行调用,rule 定义后在不同的构建步骤间共享。这是 Ninja 语法的一个例子,声明了一条名为“compile”的 rule——这条 rule 会调用 gcc 编译器,此外还有两条 build 语句对特定文件使用了 compile。

rule compile
  command = gcc -Wall -c $in -o $out
build out/foo.o: compile src/foo.c
build out/bar.o: compile src/bar.c
复制代码

第二种抽象是变量。

在上面的例子中,那些以”"为前缀的标识符就是变量("为前缀的标识符就是变量(in 和 $out)。变量即可以表示命令的输入,也可以表示命令的输出,也可以给长字符串起一个短点的名字。这里有一个compile定义,用一个变量表示编译器的标志:

cflags = -Wall
rule compile
  command = gcc $cflags -c $in -o $out
复制代码

一条规则中使用的 变量 可以在单个 build 块中被缩进表述的新定义覆盖。继续上面的例子,cflags 的值可以在单个文件处调整:

build out/file_with_extra_flags.o: compile src/baz.c
  cflags = -Wall -Wextra
复制代码

rule 与函数很像,而且变量又酷似参数。这两个简单的功能使 Ninja 的语法与编程语言过于相似,这很危险——是“不做多余事”的对立面。但他们可以减少重复字符串,这不仅对人非常有用,也有利于电脑计算,因为减少了需要分析的文本量。

构建文件,一旦完成分析,就可以描绘出一幅依赖图:最终的二进制输出依赖于一组对象文件,这组对象文件中的每个都是编译源文件的结果。特别地,这是一幅二分图(bipartite graph),“结点”(输入文件)指向“边”(构建指令),构建指令再指向“结点”(输出文件)[6]。构建过程就是遍历这幅图。

给定一个构建目标,Ninja 首先遍历这幅图以确定每条“边”上输入文件的状态:即,输入文件是否存在,以及被修改的时间。Ninja 随即计算出一份计划。计划即是为了保证最终目标处于最新状态而必须执行的“边”的集合,依据中间文件的修改时间判断。最后,执行计划,遍历图并将“边”标记为已执行,至此顺利结束。

优化 Ninja

Ninja 最初的实现仔细地组织了数据结构,为快速构建创造条件。但从优化的角度来说这并不是个聪明的想法。在程序完成之际,我想到,一个分析器(profiler)可以揭示哪些代码对性能产生重要影响。

这些年来,分析(profiling)的结果指向过程序的不同的区域。有时是单个热点程序,可以微优化(micro-optimized)。更多时候,分析会指向一些更广泛的问题,如,除非必要,不要分配或复制内存。也存在某些情型,采用更好的表示方法或数据结构可以获得最好的效果。接下来是对 Ninja 的实现的简单表述,以及围绕 Ninja 性能的有趣故事。

解析(Parsing)

起初,Ninja 使用的是手写的词法分析器和递归下降分析器。我以为语法足够简单了。事实证明,对于像 这样够大的项目仅仅是解析构建文件(拓展名以 .ninja 结尾)所消耗的时间都十分令人吃。

很快,最初用来解析单个字符的函数很快出现在分析结果中:

static bool IsIdentifierCharacter(char c) {
  return
    ('a' <= c && c <= 'z') ||
    ('A' <= c && c <= 'Z') ||
    // and so on...
}

``

一个简单的调整就可以节省 200 毫秒——用一个有 256 个条目、以输入字符为索引的查找表替换这个函数。这样一张表用 Python 代码很好生成,像这样:

```shell
cs = set()
for c in string.ascii_letters + string.digits + r'+,-./\_$':
    cs.add(ord(c))
for i in range(256):
    print '%d,' % (i in cs),
复制代码

这个技巧使 Ninja 在相当一段时间里保证了运行速度。最终我们转移到了更正式的工具:re2c,PHP所用的词法分析器(lexer)生成工具。它可以生成更复杂的查找表,和人无法理解的代码。例如:

if (yych <= 'b') {
    if (yych == '`') goto yy24;
    if (yych <= 'a') goto yy21;
    // and so on...
复制代码

当初采用文本格式作为输入格式是否是一个好主意?这一点依然不明确。或许最终我们会采用某种机器友好的格式作为 Ninja 输入文件的格式,这也许可以避免绝大部分解析工作。

规范化(Canonicalization)

Ninja 避免用字符串识别路径。取而代之的是,Ninja 将它遇到的每个路径映射到唯一的 Node 对象,在后续代码中以 Node 对象表示这一路径。复用 Node 对象保证了一个给定的路径只在硬盘上检查一次,检查的结果(例如,修改时间)可在其它代码中复用。

指向 Node 对象的指针如同这一路径的唯一标识。如果想要测试两个 Node 是否指向同一路径,比较指针就足够了,不需要进行昂贵的字符串比较。例如,在 Ninja 遍历输入文件构成的图时,会保存一个 Node 依赖栈,以检查依赖是否有环:如果 A 依赖 B,B依赖 C,而 C 又依赖 A。构建就无法进行。这个栈,代表一组文件,可以通过一个指针数组实现,可以使用指针相等性判断检查是否有重复。

为了保证指向一个文件的路径总是指向同一 Node,Ninja 必须可靠地将一个文件所有可能的名字映射到同一 Node 对象。这需要对输入文件中提到的所有文件进行规范化(canonicalization),将像 foo/../bar.h 这样的路径转换为 bar.h。最初,Ninja 只是简单地要求所有路径以规范的形式给出,但由于几个原因,这最后还是不行。一个原因是用户指定的路径(例如,在命令行输入 ninja ./bar.h )应该能正确工作。另一个原因是变量的组合可能产生出不规范的路径。最后,gcc 给出的依赖信息可能不规范。

于是,最终,Ninja 对路径进处理。这也导致路径功能成能成为分析结果中的另一处热点。原来的实现是以清晰而不是以性能为重点编写的,所以标准的优化技术,如移除双循环(removing a double loop)或避免内存分配,作用显著。

设计目标

!!! info "这里是Ninja的设计目标"

* 非常快(即瞬间)增量构建,即使是非常大的项目。
* 代码如何构建的策略非常少。关于代码如何构建不同的项目和高级构建系统有不同的意见;例如,构建目标是应该与代码放在一起还是放在单独的目录里?是否有为项目构建一个可分发的“包”的规则?规避这些策略,而不是选择,否则只会变得复杂。
* 获取正确的依赖关系,在某些特定的情况下Makefiles很难获取正确的依赖关系(例如:输出隐式依赖于命令行生成,构建c代码你需要使用gcc的-M标志去生成其头文件依赖)
* 当目标和遍历冲突时我们选择速度.
复制代码

一些明确的 non-goals:

方便人工编写构建文件的语法。你应该使用其它程序来生成你的构建文件,这是我们可以回避许多策略决定。

built-in rules. _Out of the box, Ninja has no rules for
e.g. compiling C code._

build-time customization of the build. _Options belong in
the program that generates the ninja files_.

build-time decision-making ability such as conditionals or search
paths. Making decisions is slow.
复制代码

重申一下,Ninja比其它构建系统要快,因为它是异常的简单。在你为你的项目编写.ninja文件时你必须告诉Ninja要它做什么。

与Make比较

Ninja的定位非常清晰,就是达到更快的构建速度。

ninja的设计是对于make的缺陷的考虑,认为make有下面几点造成编译速度过慢:

隐式规则,make包含很多默认 变量计算,比如编译参与应该如何计算出来 依赖对象计算 ninja认为描述文件应该是这样的:

依赖必须显式写明(为了方便可以产生依赖描述文件) 没有任何变量计算 没有默认规则,没有任何默认值 针对这点所以基本上可以认为ninja就是make的最最精简版。 ninja相对于make增加了下面这些功能:

如果构建命令发生变化,那么这个构建也会重新执行。 所依赖的目录在构建之前都已经创建了,如果不是这样的话,我们执行命令之前都要去生成目录。 每条构建规则,除了执行命令之外,还允许有一个描述,真正执行打印这个描述而不是实际执行命令。 每条规则的输出都是buffered的,也就是说并行编译,输入内容不会被搅和在一起。 构建工具太多了,我个人觉得make主要偏大众化一点,可以进行各种隐式推导,比较灵活,每一条命令执行都有输出。 而Ninja主要的设计目的是为了像chromium这种大型项目,能够显著的提高编译速度,一方面它去掉了各种计算和推导,把一些耗时的需要计算的东西去掉了,只留下简单重要的部分,所以如果自己去写build.ninja文件的话比较繁琐,所以都是依赖于其它构建工具生成的,另一方面它每次输出只输出一个描述,而不是真正的命令执行输出,真正的命令执行再后台运行,只有警告和报错信息才会显示出来,这也提高了它的速度。

Make vs Ninja Performance Comparison这篇文章对Make接Ninja进行测试对比。

在你的项目中使用ninja

Ninja 目前在Windows和类Unix系统都支持,虽然大部分测试都是在linux上完成的(并且在Linux上性能也最好),不过在MAC OS X和FreeBSD 上也都能很好的工作。 如果你的项目很小,Ninja的速度优势可能不那么明显(然而,即使是小项目,Ninja的极其简洁的语法和极其简单的构建规则,也使你的项目能够更加快速的构建)。换一句话说,如果你对你的项目编辑-编译的循环时间感到满意,那么Ninja可能对你不会有太多的帮助。 还有许多其它的构建系统,比Ninja使用更加友好,功能也更加强大。作者觉得Ninja的设计受到了tup构建系统设计的影响,并认为重做的设计非常聪明。 Ninja的好处是可以将它与智能的元构建系统结合起来使用。 gyp用于生成Google 和相关项目(v8,node.js)的元构建文件建立系统,已被GN取代。 gyp可以为支持的所有平台生成Ninja文件。 有关详细信息,请参阅Chromium Ninja文档。 CMake一个广泛使用的元构建系统,在Linux上CMake 2.8.8版本可以生成Ninja文件。 较新版本的CMake支持在Windows和Mac OS X上生成Ninja文件。 其他:Ninja应该完善其他元构建软件的支持,例如premake。 如果你在做这项工作,请让我们知道!

运行Ninja

运行ninja。 默认情况下,它在当前目录中查找名为build.ninja的文件 并构建所有过期目标。 您可以在命令行参数中指定要构建的目标(文件)。

还有一个特殊的语法,目标^指定的目标作为第一个输出(如果存在)。 例如,如果您指定目标 foo.c ^, 那么foo.o将首先被构建(假设你的构建文件中存在这些目标)。

ninja -h 打印帮助。

许多Ninja的flags与Make是匹配的; 例如ninja -C build -j 20改成构建目录并并行运行20个构建命令。 (注意 Ninja默认情况下以并行方式运行命令,所以通常你不需要传递-j。) Ninja 默认基于系统中可用的 CPU 数量以并发方式执行指令。因为同时运行的命令们的输出可能混淆,Ninja 会在一个命令完成前缓存其输出。从结果看,如同命令是串行的。 这种对命令输出的控制使得 Ninja 可以小心控制总的输出。在构建过程中 Ninja 显示一行表示状态;如果构建顺利完成,Ninja 的全部输出就只有一行。这不会使 Ninja 运行得更快,但可以使人感觉 Ninja 很快,这几乎与在真实速度上的目标一样重要。

环境变量

Ninja支持用一个环境变量来控制其行为: NINJA STATUS,在运行规则之前会打印进度状态。 下面是几个可用的占位符:

%s:: The number of started edges.
%t:: The total number of edges that must be run to complete the build.
%p:: The percentage of started edges.
%r:: The number of currently running edges.
%u:: The number of remaining edges to start.
%f:: The number of finished edges.
%o:: Overall rate of finished edges per second
%c:: Current rate of finished edges per second (average over builds
specified by -j or its default)
%e:: Elapsed time in seconds. (Available since Ninja 1.2.)
%%:: A plain % character.
复制代码

默认进度状态为 "[%f/%t] " ( 注意结尾空格以与构建规则分开). 另一个可能的进度状态的例子如下: "[%u/%r/%f] ".

扩展工具

在Ninja的开发过程中,命令行里使用-t可以运行一些非常有用的工具,目前有以下一些工具可以使用:

query:: dump指定target的输入和输出.

browse:: 在Web浏览器中浏览依赖关系图。 单击文件将焦点切到该文件上,会显示输入和输出。 这个 功能需要Python安装。 默认使用端口8000并打开Web浏览器。 可以按照如下方式修改:

ninja -t browse --port=8000 --no-browser mytarget
复制代码

graph::以自动图形布局工具graphviz的语法输出一个文件。 使用方式如下:

ninja -t graph mytarget | dot -Tpng -ograph.png
复制代码

在Ninja源代码树中,运行“ninja graph.png”命令将为Ninja本身生成一张图。 如果没有指定目标则将为 all目标生成。

编写你自己的Ninja文件

概述

Ninja和Make非常相似。他执行一个文件之间的依赖图,通过检测文件修改时间,运行必要的命令来更新你的构建目标。 一个构建文件(默认文件名为:build.ninja)提供一个rule(规则)表——长命令的简短名称,和运行编译器的方式。同时,附带提供build(构建)语句列表,表明通过rule如何构建文件——哪条规则应用于哪个输入产生哪一个输出。

从概念上讲,build语句描述项目的依赖图;而rule语句描述当给定一个图的一条边时,如何生成文件。

语法例子

这是一个用于验证绝大部分语法的.ninja文件,将作为后续描述相关的示例。具体内容,如下:

cflags = -Wall

rule cc
  command = gcc $cflags -c $in -o $out

build foo.o: cc foo.c

复制代码

变量

ninja支持为字符串声明简短可读的名字。一个声明的语法,如下:

cflags = -g

可以在=右边使用,并通过$进行引用(类似shell和perl的语法)。具体形式,如下:

rule cc
  command = gcc $cflags -c $in -o $out
复制代码

变量还可以用{in}(和成对的大括号)来引用。 当给定变量的值不能被修改,只能覆盖(shadowed)时,变量更恰当的叫法是绑定(”bindings”)。

规则

规则为命令行声明一个简短的名称。他们由关键字rule和一个规则名称打头的行开始,然后紧跟着一组带缩进格式的 variable = value行组成。 以上示例中声明了一个名为cc的rule,连同一个待运行的命令。在rule(规则)上下文中,command变量用于定义待执行的命令,in展开(expands)为输入文件列表(foo.c),而out为命令的输出文件列表(foo.o)。 参考手册中罗列了所有特殊的变量。

Build 语句

build语句声明输入和输出文件之间的一个关系。构建语句由关键字build开头,格式为

build outputs: rulename inputs 1 这样的一个声明,所有的输出文件来源于输入文件。当缺输出文件或输入文件变更时,Ninja将会运行此规则来重新生成输出。 以上的简单示例,描述了使用cc规则如何构建foo.o文件。 在build block范围内(包括相关规则的执行),变量in表示输入列表,out表示输出列表。 一个构建语句,可以和rule一样,紧跟一组带缩进格式的key = value对。当在命令中变量执行时,这些变量将覆盖(shadow)任何变量。比如:

cflags = -Wall -Werror
rule cc
  command = gcc $cflags -c $in -o $out

# 如果没有指定,build的输出将是$cflags
build foo.o: cc foo.c

# 但是,你可以在特殊的build中覆盖cflags这样的变量
build special.o: cc special.c
  cflags = -Wall

# cflags变量仅仅覆盖了special.o的范围
# 以下的子序列build行得到的是外部的(原始的)cflags
build bar.o: cc bar.c

复制代码

如果你要从build语句传递更多的信息到rule规则(例如,如果规则需要知道”第一输入文件的扩展名”),那么请通过扩展变量传递,就像cflags一样。

如果顶级Ninja文件使用build指定了任何输出,并且它又过期了,那么再为构建用户目标之前会先构建顶级文件里的目标。

根据代码生成Ninja文件

Ninja发行包中的misc/ninja_syntax.py是一个很小的python模块,用于生成Ninja文件。你可以使用python,执行如

ninja.rule(name='foo', command='bar',depfile='$out.d')

的调用,生成合适的语法。如果这样还不错,可以将其整合到你的项目中。

更多细节

phony 规则 可以使用phony创建其它target(编译构建目标)的别名。比如:

build foo: phony some/file/in/a/faraway/subdir/foo

这样使得ninja foo构建更长的路径。从语义上讲,phony规则等同于一个没有做任何操作的普通规则,但是phony规则通过特殊的方式进行处理,这样当其运行时不会被打印,记日志,也不作为构建过程中打印出来的命令计数。 还可以用phony为构建时可能还不存在的文件创建dummy目标。

默认目标 默认情况下,如果没有在命令行中指定target,那么Ninja将构建任何地方没有作为输入命名的每一个输出。可以通过default目标语句来重写这个行为。一个default语句,让Ninja构建一个给定的输出文件子集,如果命令行中没有指定构建目标。 默认目标语句,由关键字default打头,并且采用default targets的格式。一个default目标语句必须出现在,声明这个目标作为一个输出文件的构建语句之后。他们是累积的(cumulative),所以可以使用多个default语句来扩展默认目标列表。比如:

default foo bar default baz

This causes Ninja to build the foo, bar and baz targets by default.

Ninja 日志

Ninja构建日志保存在构建过程的根目录或.ninja文件中builddir变量对应的目录的.ninja_log文件中。 一般而言,像上面这样的微优化不如改变算法或处理方式的结构性优化有效。Ninja 的构建日志就是这样一个例子。 Linux kernel 构建系统的一部分会追踪用于生成输出的命令。考虑一个启发性的例子:你将输入文件 foo.c 编译为输出文件 foo.o,随后修改了构建文件导致应该用不同的编译选项重新编译 foo.c。从构建系统的角度看,为了感知需要构建,必须要么注意到 foo.o 依赖于构建文件(构建文件依赖于项目的组织,这也许意味着对构建文件的修改将导致整个项目的重新构建),或记录生成每个输出的命令,在每次构建时进行比较。 kernel(以及 Makefiles 和 Ninja)采用后一种方法。在构建时,Ninja 写下一份构建日志,记录生成每个输出的完整命令。[9]在后续构建中,Ninja 载入之前的构建日志,通过比较当前命令与构建日志中的命令来发现变更。就像加载构建文件或路径规范化,这成为了分析结果中的又一处热点。 在进行了一些小优化后,Nico Weber,一个对 Ninja 贡献了很多代码的人,实现一种新的构建日志格式。比起通常很长且需要大量时间进行解析的记录命令,Ninja 取而代之以命令的哈希(hash)。在后续构建中,Ninja 比较将要执行的明令的哈希与记录中的哈希。如果两者不同,则可以确定输出已过期。这一方法很成功。使用哈希急剧降低了构建日志的大小——在 Mac OX X 上,从 200MB 降到 2MB——并使加载速度快了 20 倍。

版本兼容性

Available since Ninja 1.2.

Ninja version labels follow the standard major.minor.patch format,
where the major version is increased on backwards-incompatible
syntax/behavioral changes and the minor version is increased on new
behaviors. Your build.ninja may declare a 变量 named
ninja_required_version that asserts the minimum Ninja version
required to use the generated file. For example,

ninja_required_version = 1.1
1
declares that the build file relies on some feature that was
introduced in Ninja 1.1 (perhaps the pool syntax), and that
Ninja 1.1 or greater must be used to build. Unlike other Ninja
变量s, this version requirement is checked immediately when
the 变量 is encountered in parsing, so it’s best to put it
at the top of the build file.

Ninja always warns if the major versions of Ninja and the
ninja_required_version don’t match; a major version change hasn’t
come up yet so it’s difficult to predict what behavior might be
required.
复制代码

文件依赖

还有另一种元数据(metadata)必需跨构建保存用。为了正确构建 C/C++ 代码,一个构建系统必需能感知头文件间的依赖。假定 foo.c 包含一行 #inclue “bar.h” 。而 bar.h 自身又包含一行 #include “bar.h”。所有的三个文件都会影响后续编译。例如,baz.h 的改变也会触发 foo.o 的重新构建。

一些构建系统使用一个“头文件扫描器”在构建时提取这部分依赖信息。但这个方法太慢,而且很难精确处理有 #ifdef 指令出现的情形。另一种选择是要求构建文件正确地报告所有依赖,包括头文件的依赖,但这对开发人员来说十分笨重:每次你添加或删除 #include 语句时,都需要修改或重新生成构建文件。

一个有用的方法依赖于这样的事实:在编译时,gcc (以及微软的 Visual Studio)可以给出在构建输出时用到了哪些头文件。这份信息,如同用于生成输出的信息,可以被构建系统记录和加载。由此,依赖可以被精确追踪。在第一次编译时,因为还未有输出,所有文件都会被编译,故不需头文件依赖。第一次编译后,对于被某个输出用到的任何文件如果发生更改(包括增加或删除额外的依赖),就会导致重新构建。这保证了依赖信息的更新。

在编译时,gcc 以 Makefile 的格式记下头文件依赖。Ninja 包括一个解析器处理这一Makefile 语法(的简化子集),并在下一次构建时载入这份依赖信息。在 的最近一次构建,gcc 产生了共 90MB 的 Makefile,全部带有必须规范化的引用路径。

就像其它解析过程,通过使用 re2c 及尽可能地避免复制可以使性能有所提升,但就像 GYP 项目,这一解析工作可以不在关键时间路径上完成。近期,我们在 Ninja 上的工作(在写作本文时,这一工能已经完成,但还未发布)是让这一过程发生的早一些。

一旦 Ninja 开始执行构建指令,所有影响性能的工作都已完成,Ninja 在等待它启动的命令完成的过程中近乎闲置。在处理头文件依赖的新方法中,Ninja 利用这段时间处理 gcc 给出的 Makefile ,规范化路径,将依赖处理为一种可以快速识别的二进制格式。在下一次构建中,Ninja 只需要加载这一文件。改进非常剧烈,特别是在 Windows 上。(本章稍后讨论这个)

“依赖日志”需要储存上千条路径及路径间的依赖。载入日志和追加日志都必须迅速。追加日志操作应该是安全的,即使被打断,比如构建被取消。

在考虑了一些类似于数据库的方案后,我最终想到了一个简单的实现:文件由记录的序列组成,而记录要么是一个路径,要么是一个依赖列表。每个写入文件的路径都被赋于了一个整数序列号。故而依赖就是一列整数。为了向文件添加依赖,Ninja 首先记录下还没有序列号的路径,然后用这些序列号记录依赖。在后续的构建载入这一文件时,Ninja 可以简单地使用一个数组将序列号映射到对应的 Node 指针。

C/C++ 头依赖

Ninja目前支持depfile和deps模式的C/C++头文件依赖生成。 如

rule cc
  depfile = $out.d
  command = gcc -MMD -MF $out.d [other gcc flags here]
复制代码

-MMD标识告诉gcc要生成头文件依赖,-MF则说明要写到哪里。 deps按照编译器的名词来管理。具体如下:(针对微软的VC:msvc)

rule cc
  deps = msvc
  command = cl /showIncludes -c $in /Fo$out
复制代码

工作池

为了支持并发作业,Ninja还支持pool的机制(和用-j并行模式一样)。此处不详细描述了。具体示例,如下:

# No more than 4 links at a time.
pool link_pool
  depth = 4

# No more than 1 heavy object at a time.
pool heavy_object_pool
  depth = 1

rule link
  ...
  pool = link_pool

rule cc
  ...

# The link_pool is used here. Only 4 links will run concurrently.
build foo.exe: link input.obj

# A build statement can be exempted from its rule's pool by setting an
# empty pool. This effectively puts the build statement back into the default
# pool, which has infinite depth.
build other.exe: link input.obj
  pool =

# A build statement can specify a pool directly.
# Only one of these builds will run at a time.
build heavy_object1.obj: cc heavy_obj1.cc
  pool = heavy_object_pool
build heavy_object2.obj: cc heavy_obj2.cc
  pool = heavy_object_pool
复制代码

console 池

这里有一个名为console深度为1的预定义池,池中的任何任务都可以直接访问标准输入、输出和错误流并提供给Ninja,通常是连接到用户的控制台。这对于交互式任务或运行时间较长的任务比较有用。可以在控制台上更新状态(例如测试套件)。 当’console’池中的任务正在运行时,Ninja的正常输出(如进度状态和并发任务的输出)将被缓冲起来直到控制台任务运行完成。

Ninja 文件参考

一个Ninja文件是一系列声明,声明可以是下列之一:

规则声明,以rulename开头,然后是一些列的变量的定义;

A build edge, which looks like +build output1 output2:
rulename input1 input2+. +
Implicit dependencies may be tacked on the end with +|
dependency1 dependency2+. +
Order-only dependencies may be tacked on the end with +||
dependency1 dependency2+. (See <
复制代码

语法

Ninja仅支持ASCII字符集。 注释以为#开始一直到行末。

新行是很重要的。像build foo bar的语句,是一堆空格分割分词(token),到换行结束。一个分词中的新行和空格必须进行转译。目前只有一个转译字符,$,其具有以下行为:

$ followed by a newline

转译换行,让当前行一直扩展到下一行。

$ followed by text

这是, 变量引用。

${varname}

这是,另$varname的另一种语法。

$ followed by space

这表示一个空格。(仅在path列表中,需要用空格分割文件名)

这表示一个冒号。(仅在build行中需要。此时冒号终止输出列表)

这个表示,字面值的$。

一个build或default语句,最先被解析,作为一个空格分割的文件名列表,然后每一个name都被展开。也就是说,变量中的一个空格将作为被展开后文件名中的一个空格。

spaced = foo bar
build $spaced/baz other$ file: ...

在一个name = value语句中,value前的空白都会被去掉。出现跨行时,后续行起始的空白也会被去掉。

two_words_with_one_space = foo $
    bar
one_word_with_no_space = foo$
    bar
复制代码

其他的空白,仅位于行开始处的很重要。如果一行的缩进比前一行多,那么被人为是其父边界的一部分。如果缩进比前一行少,那他就关闭前一个边界。

顶层变量

Ninja支持的顶层变量有builddir和ninja_required_version。具体说明,如下:

builddir: 构建的一些输出文件的存放目录。 ninja_required_version:指定满足构建需求的最小Ninja版本。 Rule 变量 一个rule块包含一个key = value的列表声明,这直接影响规则的处理。以下是一些特殊的key:

command (required):
待执行的命令。这个字符串 $variables被展开之后,被直接传递给sh -c,不经过Ninja翻译。每一个规则只能包含一条command声明。如果有多条命令,需要使用&&符号进行链接。
depfile: 指向一个可选的Makefile,其中包含额外的隐式依赖。这个明确的为了支持C/C++的头文件依赖。
deps: (1.3版本开始支持)如果存在,必须是gcc或msvc,来指定特殊的依赖。产生的数据库保存在builddir指定目录.ninja_deps文件中。
msvc_deps_prefix: (1.5版本开始支持)定义必须从msvc的/showIncludes输出中去掉的字符串。仅在deps = msvc而且使用非英语的Visual Studio版本时使用。
description: 命令的简短描述,作为命令运行时更好的打印输出。打印整行还是对应的描述,由-v标记控制。如果一个命令执行失败,整个命令行总是在命令输出之前打印。
generator: 如果存在,指明这条规则是用来重复调用生成器程序。通过两种特殊的方式,处理使用生成器规则构建文件:首先,如果命令行修改了,他们不会重新构建;其次,默认不会被清除。
in: 空格分割的文件列表被作为一个输入传递给引用此rule的构建行,如果出现在命令中需要使用${in}(shell-quoted)。(提供$in仅仅为了图个方便,如果你需要文件列表的子集或变种,请构建一个新的变量,然后传递新的变量。)
in_newline: 和$in一样,只是分割符为换行而不是空格。(仅为了和$rspfile_content一起使用,解决MSVC - linker使用固定大小的缓冲区处理输入,而造成的一个bug。)
out: 空格分割的文件列表被作为一个输出传递给引用此rule的构建行,如果出现在命令中需要使用${out};
restat: 如果存在,引发Ninja在命令行执行完之后,重新统计命令的输出。
rspfile, rspfile_content: 如果存在(两个同时),Ninja将为给定命令提供一个响应文件,比如,在调用命令之前将选定的字符串(rspfile_content)写到给定的文件(rspfile),命令执行成功之后阐述文件。
这个在Windows系统非常有用,因为此时命令行的最大长度非常受限,必须使用响应文件替代。具体使用方式,如下:
rule link
  command = link.exe /OUT$out [usual link flags here] @$out.rsp
  rspfile = $out.rsp
  rspfile_content = $in

build myapp.exe: link a.obj b.obj [possibly many other .obj files]
复制代码

command变量的解释

在Unixes和Windows上命令行的行为是不同的。

在Unixes上,命令是参数数组。 Ninja命令变量直接传递给sh -c,然后负责 将该字符串解释为argv数组。 因此引用规则由shell决定,你可以使用所有正常的shell 运算符,如链接多个命令,或VAR = value cmd 来设置环境变量。

在Windows上,命令是字符串,因此Ninja直接将command字符串 传递给CreateProcess。 (在常见情况下编译器简单执行这意味着有更少的开销。)因此引用规则由被调用的程序确定,在Windows上通常由C库提供。 如果你需要shell解释命令(如使用&&来链接多个命令),使命令执行Windows shell前缀命令与cmd / c。

构建输出

有两种稍微有点区别的输出:

显示输出, 在build行会列出来,在rule规则中可以通过$out变量访问。 这是标准的输出使用形式,例如一个编译命令的目标文件。

隐式输出, 在build行其语法格式如下,在build行的:前out1 out2(在Ninja1.7版本开始支持).语义与显式输出相同,唯一的区别是隐式输出不会出现在$out变量里。这是为了表示在命令行中没有指定输出的命令。

构建依赖

Ninja目前支持3种类型的构建依赖。分别是:

罗列在build行中的显式的依赖。他们可以作为规则中的in变量。这是标准依赖格式。
从depfile属性或构建语句末尾的| dep1 dep2语法获得的隐式依赖。这个和显式依赖一样,但是不能在in中使用(不可见)。 通过构建行末|| dep1 dep2语法表示的次序唯一(Order-only)依赖。他们过期的时候,输出不会被重新构建,直到他们被重建,但仅修改这种依赖不会引发输出重建。 变量展开 变量在路径(在build或default语句)和name = value右边被展开。 当name = value语句被执行,右手边的被立即展开(根据以下的规则),从此name扩展为被展开结果的静态字符串。永远也不会存在,你将需要使用双转译("double-escape")来保护一 个值被第二次展开。
所有变量在解析过程,遇到的时候立即被展开,除了一个非常重要的例外:rule块中的变量仅在规则被使用的时候才被展开,而不是声明的时候。在以下的示例中,demo打印出"this is a demo of bar"而不是"this is a demo offoo"。

rule demo
  command = echo "this is a demo of $foo"

build out: demo
  foo = bar
复制代码

评估和边界

顶层(Top-level)变量声明的边界,是相关的文件。 subninja关键自,用于包含另一个.ninja文件,其表示新的边界。被包含的subninja文件可以使用父文件中的变量,在文件边界中覆盖他们的值,但是这不影响父文件中变量的值。 同时,可以用#include语句在当前边界内,引入另一个.ninja文件。这个有点像C中的#include语句。 构建块中声明的变量的边界,就是其所属的块。一个构建块中展开的变量的所有查询次序为:

特殊内建变量(in,out); build/rule块中构建层的变量; 构建行所在文件中的文件层变量(File-level); 使用subninja关键字引入那个文件的(父)文件中的变量。

总结

在上一篇GN构建系统中主要介绍了GN是一个构建编译文件相关依赖和配置参数的工具,主要是静态的生产Ninja需要的构建文件 Ninja主要是分析文件的内部内容,和分析文件内容的异常情况和依赖,更加关注系统和编译环境的配置和出来

GN:一个静态文件依赖关系和系统,CPU工具链相关的依赖关系(静态关系) Ninja:在GN生成的文件作为基础,出来开发过程中文件的变动,源代码进行检查,优化,最终生成可执行文件

note: ·