ninja介绍及使用
Posted 既然如此先吃饭吧
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ninja介绍及使用相关的知识,希望对你有一定的参考价值。
ninja
简介
ninja 是Google
的一名程序员推出的注重速度的构建工具.一般在Unix/Linux上的程序通过make/makefile
来构建编译,而ninja
通过将编译任务并行组织,大大提高了构建速度。
执行
ninja [-options] targets
支持参数
--version # 打印版本信息
-v # 显示构建中的所有命令行(这个对实际构建的命令核对非常有用)
-C DIR # 在执行操作之前,切换到`DIR`目录
-f FILE # 制定`FILE`为构建输入文件。默认文件为当前目录下的`build.ninja`。如 ./ninja -f demo.ninja
-j N # 并行执行 N 个作业。默认N=3(需要对应的CPU支持)。如 ./ninja -j 2 all
-k N # 持续构建直到N个作业失败为止。默认N=1
-l N # 如果平均负载大于N,不启动新的作业
-n # 排练(dry run)(不执行命令,视其成功执行。如 ./ninja -n -t clean)
-d MODE # 开启调试模式 (用 -d list 罗列所有的模式)
-t TOOL # 执行一个子工具(用 -t list 罗列所有子命令工具)。如 ./ninja -t query all
-w FLAG # 控制告警级别
特点
- 可以通过其他高级的编译系统生成其输入文件;
- 它的设计就是为了更快的编译;
通过其他高级的编译系统生成其输入文件
Ninja与android
安卓编译系统演进历史
Google
在 Android 7.0
之前都是使用的makefile
进行编译,7.0开始引入了Soong构建系统
,旨在取代make
,它利用 Kati GNU Make 克隆工具
和 ninja 构建系统组件
来加速 Android
的构建。
生成ninja文件
- 工具链关系
Android.bp --> Blueprint --> Soong --> ninja
Makefile or Android.mk --> kati --> ninja
(Android.mk --> Soong --> Blueprint --> Android.bp)
在编译过程中,Android.bp
会被收集到out/soong/build.ninja.d
,blueprint
以此为基础,生成out/soong/build.ninja
Android.mk
会由kati
/ckati
生成为out/build-aosp_arm.ninja
两个ninja文件会被整合进入out/combined-$product_$arch.ninja
- combined-$product_$arch.ninja
$ source build/envsetup.sh
$ lunch pixel3_mainline-userdebug
$ make nothing
$ cat out/combined-pixel3_mainline.ninja
builddir = out
pool highmem_pool
depth = 2
subninja out/build-pixel3_mainline.ninja
subninja out/build-pixel3_mainline-package.ninja
subninja out/soong/build.ninja
- 执行
prebuilts/build-tools/linux-x86/bin/ninja \\
-f out/combined-pixel3_mainline.ninja
ninja本身
ninja本身就是通过ninja编译出来的
源码获取及编译
git clone https://android.googlesource.com/platform/external/ninja
python3 configure.py --bootstrap
编译过程
- 生成一个build.ninja
- 执行python3 configure.py --bootstrap之后编译源码,生成一个a.out
- 根据这个build.ninja重新编译生成可执行文件ninja
- 在 ninja 根据 ninja.build 来编译时会自动创建一个 build 目录用于存放编译过程中的临时文件
ninja_syntax.py
Ninja提供了一个简单的生成脚本,它实际上是一个python模块misc/ninja_syntax.py
,通过它我们可以较方便的生成build.ninja
文件
from ninja_syntax import Writer
with open("build.ninja", "w") as buildfile:
n = Writer(buildfile)
if platform.is_msvc():
n.rule("link",
command="$cxx $in $libs /nologo /link $ldflags /out:$out",
description="LINK $out")
else:
n.rule("link",
command="$cxx $ldflags -o $out $in $libs",
description="LINK $out")
为了更快的编译
与makefile的对比
$ time ninja
ninja 39.24s user 2.16s system 1021% cpu 4.053 total
3.79s
$ time make
make 22.29s user 1.59s system 101% cpu 23.543 total
23.13s
语法及概念
- edge(边):build语句,可以指定目标(target)输出(output)、规则(rule)与输入(input)
- target(目标):编译过程需要生成的目标,由build语句指定
- output(输出):build语句的前半段,是target的另一种称呼
- input(输入):build语句的后半段,用于产生output的文件或目标,另一种称呼是依赖
- rule(规则):通过指定command与一些内置变量,决定如何从输入产生输出
- pool(池):一组rule或edge,通过指定其depth,可以控制并行上限
- scope(作用域):变量的作用范围,有rule与build语句的块级,也有文件级别。
Gcc = gcc # 全局变量
# rule
rule name # name是rule名
command = $Gcc $in > $out # 执行命令
var = str # 局部变量
# Edge
# output0 output1 显示输出
# output2 output3 隐式输出
# rule_name 规则名称
build output0 output1 | output2 output3: rule_name $
input0 input1 $ # 显示依赖
| input2 input3 $ # 隐式依赖
|| input4 input5 # order-only依赖 可有可无
var0 = str0
var1 = str1
这张图中src/browse.py
src/inline.sh
就是input
也就是依赖
,inline
就是rule
,build/browse_py.h
就是output
也就是目标(target)
,上述组合起来的就是一个edge
而在其他的edge
中,build/browse_py.h
又会作为input
再比如src/util.h
会同时作为build/build.o
和build/log.o
的input
整个图就是一个scope
底层的数据结构
回到这张图,我们来看Ninja的底层的如何处理的(以下数据结构只保留到最简的部分)
State
State
保存单次运行的全局状态
struct State
//内置pool和Rule使用这个虚拟的内置scope来初始化它们的关系位置字段。这个范围内没有任何东西。
static Scope kBuiltinScope;
static Pool kDefaultPool;
static Pool kConsolePool;
static Rule kPhonyRule;
// 内置的hashmap 保存所有的Node
typedef ConcurrentHashMap<HashedStrView, Node*> Paths;
Paths paths_;
// 保存所有的Pool
std::unordered_map<HashedStrView, Pool*> pools_;
// 保存所有的edge
vector<Edge*> edges_;
// 根作用域
Scope root_scope_ ScopePosition ;
vector<Node*> defaults_; // 默认目标
private:
/// Position 0 is used for built-in decls (e.g. pools).
DeclIndex dfs_location_ = 1;
;
Scope
Scope
作用域:变量的作用范围,有rule与build语句的块级,也有文件级别。包含Rule,同时保存了父Scope的位置
struct Scope
Scope(ScopePosition parent) : parent_(parent)
private:
ScopePosition parent_; // 父位置
DeclIndex pos_ = 0; // 自己的哈希位置
// 变量
std::unordered_map<HashedStrView, std::vector<Binding*>> bindings_;
// Rule
std::unordered_map<HashedStrView, Rule*> rules_;
;
Rule
Rule
文件的构建规则,存在局部变量
struct Rule
Rule()
struct
// 该规则在其源文件中的位置。
size_t rule_name_diag_pos = 0;
parse_state_;
RelativePosition pos_; // 偏移值
HashedStr name_; // 规则名
std::vector<std::pair<HashedStr, std::string>> bindings_;//保存局部变量
;
Binding & DefaultTarget
Binding
以键值对的形式存在用来变量
DefaultTarget
保存默认的输出的target
struct Binding
RelativePosition pos_; // 偏移位置
HashedStr name_; //变量名
StringPiece parsed_value_; // 变量值
;
struct DefaultTarget
RelativePosition pos_; // 偏移值
LexedPath parsed_path_; // StringPiece
size_t diag_pos_ = 0;
;
Node
Node
是最边界的数据结构,ninja语法中的input
,output
,target
,default
的底层保存都是Node
struct Node
Node(const HashedStrView& path, uint64_t initial_slash_bits)
: path_(path),
first_reference_( kLastDeclIndex, initial_slash_bits )
~Node();
private:
// 路径值
const HashedStr path_;
std::atomic<NodeFirstReference> first_reference_;
// 作为output所在的Edge位置
Edge* in_edge_ = nullptr;
// 使用此Node作为输入的所有Edge.列表顺序不确定,每次访问都是对其重新排序
struct EdgeList
EdgeList(Edge* edge=nullptr, EdgeList* next=nullptr)
: edge(edge), next(next)
Edge* edge = nullptr;
EdgeList* next = nullptr;
;
std::atomic<EdgeList*> out_edges_ nullptr ;
std::atomic<EdgeList*> validation_out_edges_ nullptr ;
std::vector<Edge*> dep_scan_out_edges_;
;
Edge
Edge
是最核心的数据结构,会将Node
Rule
Binding
等数据结构组合起来
struct Edge
// 固定的属性值 在Rule下进行配置
struct DepScanInfo
bool valid = false;
bool restat = false;
bool generator = false;
bool deps = false;
bool depfile = false;
bool phony_output = false;
uint64_t command_hash = 0;
;
public:
struct
StringPiece rule_name; // 保存rule_name
size_t rule_name_diag_pos = 0;
size_t final_diag_pos = 0;
parse_state_;
const Rule* rule_ = nullptr; // 使用的rule
Pool* pool_ = nullptr; // 所在的pool
// 在一个edge中的input,output
vector<Node*> inputs_;
vector<Node*> outputs_;
std::vector<std::pair<HashedStr, std::string>> unevaled_bindings_; // 存储局部变量值
int explicit_deps_ = 0; // 显式输入
int implicit_deps_ = 0; // 隐式输入
int order_only_deps_ = 0; // 隐式order-only依赖
int explicit_outs_ = 0; // 显示输出
int implicit_outs_ = 0; // 隐式输出
;
如何区分显隐式,input和output会按照按照 显式 -> 隐式 -> order-only(仅依赖) 的顺序进行push_back()
根据当前的值的位置与显隐式的数量做对比就可以知道
edge->outputs_.reserve(edge->explicit_outs_ + edge->implicit_outs_);
edge->inputs_.reserve(edge->explicit_deps_ + edge->implicit_deps_ +
edge->order_only_deps_);
启动过程
入口函数
ninja.cc main() -> real_mian()
1 处理参数
- -f 选择文件
- -C 工作路径
- -t 选择内置工具
NORETURN void real_main(int argc, char** argv)
BuildConfig config;
Options options = ;
options.input_file = "build.ninja";
options.dupe_edges_should_err = true;
// 处理参数
int exit_code = ReadFlags(&argc, &argv, &options, &config); // return 1 exit
...
struct Options
// 文件 -f
const char* input_file;
// 工作路径 -C
const char* working_dir;
// 工具 -t
const Tool* tool;
// 针对一个目标的重复规则是否应该发出警告或打印错误
bool dupe_edges_should_err;
// 假周期是否应该警告或打印一个错误。
bool phony_cycle_should_err;
// 在不同的行上有多个目标的删除文件是否应该警告或打印错误。
bool depfile_distinct_target_lines_should_err;
// 是否保持持久
bool persistent;
;
2 读取ninja文件并构建图
static std::vector<ParserItem> ParseManifestChunks(const LoadedFile& file,
ThreadPool* thread_pool)
...
for (std::vector<ParserItem>& chunk_items :
ParallelMap(thread_pool, chunk_views, [&file](StringPiece view)
std::vector<ParserItem> chunk_items;
manifest_chunk::ParseChunk(file, view, &chunk_items); // 解析build.ninja
return chunk_items;
))
std::move(chunk_items.begin(), chunk_items.end(),
std::back_inserter(result));
...
再执行 ParseFileInclude
class ChunkParser
const LoadedFile& file_;
Lexer lexer_;
const char* chunk_end_ = nullptr;
std::vector<ParserItem>* out_ = nullptr; // 保存include和subninja的文件及Clump
Clump* current_clump_ = nullptr; // 读取文件并分析保存文件中的内容
;
class Clump
std::vector<Binding*> bindings_; // 保存全局变量
std::vector<Rule*> rules_; // rule
std::vector<Pool*> pools_; // pool
std::vector<Edge*> edges_; // Edge
std::vector<DefaultTarget*> default_targets_; // default
;
struct ParserItem
enum Kind
kError, kRequiredVersion, kInclude, kClump
;
Kind kind;
union
Error* error;
RequiredVersion* required_version;
Include* include;
Clump* clump;
u;
ParserItem(Error* val) : kind(kError) u.error = val;
ParserItem(RequiredVersion* val) : kind(kRequiredVersion) u.required_version = val;
ParserItem(Include* val) : kind(kInclude) u.include = val;
ParserItem(Clump* val) : kind(kClump) u.clump = val;
;
ChunkParser::ParseChunk()
此函数为读取文件进行初步分析
的主要位置,按行,循环执行lexer_.ReadToken()
;读取 build.ninja
的内容并根据内容返回枚举属性值,判断属性值并执行对应的函数
bool ChunkParser::ParseChunk()
while (true)
if (lexer_.GetPos() >= chunk_end_)
assert(lexer_.GetPos() == chunk_end_ &&
"lexer scanned beyond the end of a manifest chunk");
return true;
Lexer::Token token = lexer_.ReadToken();
bool success = true;
switch (token)
case Lexer::INCLUDE: success = ParseFileInclude(false); break;
case Lexer::SUBNINJA: success = ParseFileInclude(true); break;
case Lexer::POOL: success = ParsePool(); break;
case Lexer::DEFAULT: success = ParseDefault(); break; // 读取默认
case Lexer::IDENT: success = ParseBinding(); break; // 读取全局变量并保存
case Lexer::RULE: success = ParseRule(); break; // 读取rule , rule保存在clump->rule_中, 在遇到rule内变量时,会保存到rule->bending_ 以键值对的形式顺序保存
case Lexer::BUILD: success = ParseEdge(); break; // 获取Edge,一个build就是一个Edge
case Lexer::NEWLINE: break;
case Lexer::ERROR: return LexerError(lexer_.DescribeLastError());
case Lexer::TNUL: return LexerError("unexpected NUL byte");
case Lexer::TEOF:
assert(false && "EOF should have been detected before reading a token");
break;
default:
return LexerError(std::string("unexpected ") + Lexer::TokenName(token));
if (!success) return false;
return false; // not reached
- ParseFileInclude(false) : 处理
include
,保存文件到ChunkParser::out_
- ParseFileInclude(true) : 处理
subninja
,逻辑同上,区别在于作用域不同 - ParsePool() : 保存
pool
到Clump::pools_
- ParseDefault() : 保存
default
到Clump::default_targets_
- ParseBinding() : 保存
全局变量
到Clump::bindings_
- ParseRule() : 保存
Rule
到Clump::rule_
- ParseEdge() : 保存
Edge
到Clump::redges_
3 构建Edge图
在初步加载分析后,会执行ManifestLoader::FinishLoading(std::vector<Clump*>&,std::string*)
在再次分析得到准确的Edge
和Node
,将其保存到State
,分为5部分
bool ManifestLoader::FinishLoading(const std::vector<Clump*>& clumps,
std::string* err)
// 构造输入/输出节点的初始图。
// 选择一个可能保持碰撞次数较低的初始大小。
// Edge的非隐式输出的数量对于最终的节点的数量是一个足够好的代理。
METRIC_RECORD(".ninja load : edge setup");
size_t output_count = 0;
// 计算edge的数量
for (Clump* clump : clumps)
output_count += clump->edge_output_count_;
// 重新计算Node的容器大小,默认算Edge的三倍
state_->paths_.reserve(state_->paths_.size() + output_count * 3);
if (!PropagateError(err, ParallelMap(thread_pool_, clumps,
[this](Clump* clump)
std::string err;
// 抽出Clump中的Edge,Node,Pool等数据,初步构建Edge图
FinishAddingClumpToGraph(clump, &err);
return err;
)))
return false;
// 记录由一条边构建的每个节点的内边。检测到重复的Edge。
// 使用 dupbuild=warn(默认直到1.9.0),当两条Edge生成同一Node时,从后面的Edge的输出列表中删除重复的Node。
// 如果删除了一条Edge的所有输出,请从graph中删除该Edge。
// 简单的说就是会遍历Edge和其中的output的Node,查看是否有重复值如果有就会删除掉
METRIC_RECORD(".ninja load : link edge outputs");
for (Clump* clump : clumps)
for (size_t edge_idx = 0; edge_idx < clump->edges_.size(); )
Edge* edge = clump->edges_[edge_idx];
for (size_t i = 0; i < edge->outputs_.size(); )
Node* output = edge->outputs_[i];
if (output->in_edge() == nullptr)
output->set_in_edge(edge);
++i;
continue;
// 存在两个Edge输出同一节点
if (options_.dupe_edge_action_ == kDupeEdgeActionError)
return DecorateError(clump->file_,
edge->parse_state_.final_diag_pos,
"multiple rules generate " + output->path() +
" [-w dupbuild=err]", err
参考技术A
在 文章 中已经分析openharmony的小型系统(liteos-a)编译过程,最主要的就是调用gn/ninja/makefs三个命令最终生成可烧录的镜像文件
在 文章 详细介绍了liteos-a系统编译时gn命令的原理,本文中针对liteos-a编译时ninja的使用做一些详细说明
通过前面文章中可以看到调用的ninja命令如下
ninja工具源码及文档路径为 https://github.com/ninja-build/ninja
网上也有很多中文版的资料,如 Ninja - chromium核心构建工具
类似gn工具需要在根目录下有一个.gn以及BUILD.gn文件,ninja工具运行需要根目录下有一个build.ninja文件,也即ninja编译规则的入口,此文件所在路径可以通过选项 -C dir 来指定,例如liteos-a中使用的就是gn的out路径 /home/itsenlin/code/ohos_3.0/out/hispark_taurus/ipcamera_hispark_taurus
此文件是ninja编译工具的入口,类似于make的makefile;相对于makefile,ninja文件规则、依赖更简单,编译速度也会更快
打开此文件可以看到主要包含以下几块内容
这样在执行ninja命令时,就会默认build这个 all ,然后根据前面这个依赖关系进而编译整个系统
但是有一个问题,查看这个文件内容,并没有看到各模块目录下ninja的信息,这个是怎么关联进来的呢?
查看这个文件内容,这里面不仅仅有编译工具链相关的定义,还有对编译所需要的各模块的 .ninja 文件的关联也在这里,以及build.gn中定义的一些规则也转换成rule放在这个文件中了,如下
这样就关联上了编译所需要的所有的ninja文件,然后 ninja就会对每个依赖项进行编译,最终生成 .o 、 .a 、 .so 、 .bin 等文件
当前openharmony编译系统中不仅仅使用了gn+ninja,还使用了make+makefile。
举个例子,linux kernel的编译就是通过make编译的;而liteos kernel两者都有用,编译的文件也不一样,后面再详细分析吧
是怎么实现使用两种编译工具同时编译的呢?看生成的ninja文件(或者gn中的定义)不难看出是通过执行bash脚本来实现的,以liteos-a的make为例:
gn中有下面这个定义
转换成ninja规则如下
从上面定义看,就是ninja在编译到这个build的时候就会执行 //kernel/liteos-a/build.sh 脚本,而此脚本最后就是执行的 make 命令,如下
以上是关于ninja介绍及使用的主要内容,如果未能解决你的问题,请参考以下文章
[openharmony]liteos-a系统编译之ninja
使用ninja -C out/Release指令编译安卓Webrtc的SDK报错找不到android ndk处理方式
错误记录Android Studio 编译报错 ( A problem occurred starting process ‘command ‘ninja.exe‘ ‘ )