ninja介绍及使用

ninja介绍及使用ninja 简介 ninja 是 Google 的一名程序员推出的注重速度的构建工具 一般在 Unix Linux 上的程序通过 make makefile 来构建编译 而 ninja 通过将编译任务并行组织 大大提高了构建速度 执行 ninja options targets 支持参数 version 打印版本信息 v

大家好,我是讯享网,很高兴认识大家。

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 # 控制告警级别 

特点

  1. 可以通过其他高级的编译系统生成其输入文件;
  2. 它的设计就是为了更快的编译;

通过其他高级的编译系统生成其输入文件

Ninja与Android

安卓编译系统演进历史

在这里插入图片描述
讯享网
GoogleAndroid 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 
编译过程
  1. 生成一个build.ninja
  2. 执行python3 configure.py --bootstrap之后编译源码,生成一个a.out
  3. 根据这个build.ninja重新编译生成可执行文件ninja
  4. 在 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") 

为了更快的编译

ninja启动过程

与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.obuild/log.oinput
整个图就是一个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; // 隐式输出 }; 

讯享网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() : 保存poolClump::pools_
  • ParseDefault() : 保存defaultClump::default_targets_
  • ParseBinding() : 保存全局变量Clump::bindings_
  • ParseRule() : 保存RuleClump::rule_
  • ParseEdge() : 保存EdgeClump::redges_
3 构建Edge图

在初步加载分析后,会执行ManifestLoader::FinishLoading(std::vector<Clump*>&,std::string*)在再次分析得到准确的EdgeNode,将其保存到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); } else { 
    if (!quiet_) { 
    Warning("multiple rules generate %s. " "builds involving this target will not be correct; " "continuing anyway [-w dupbuild=warn]", output->path().c_str()); } if (edge->is_implicit_out(i)) --edge->implicit_outs_; else --edge->explicit_outs_; edge->outputs_.erase(edge->outputs_.begin() + i); } } if (edge->outputs_.empty()) { 
    clump->edges_.erase(clump->edges_.begin() + edge_idx); continue; } ++edge_idx; } } } // 此时所有的重复Edge已经被剔除掉了,现在开始给input添加需要自己的edge { 
    METRIC_RECORD(".ninja load : link edge inputs"); ParallelMap(thread_pool_, clumps, [](Clump* clump) { 
    for (Edge* edge : clump->edges_) { 
    for (Node* input : edge->inputs_) { 
    input->AddOutEdge(edge); } for (Node* validation : edge->validations_) { 
    validation->AddValidationOutEdge(edge); } } }); } // 添加默认的target { 
    METRIC_RECORD(".ninja load : default targets"); for (Clump* clump : clumps) { 
    // 从Clump->default_targets_中获取没有 DefaultTarget for (DefaultTarget* target : clump->default_targets_) { 
    std::string path; EvaluatePathInScope(&path, target->parsed_path_, target->pos_.scope_pos()); uint64_t slash_bits; // Unused because this only does lookup. std::string path_err; if (!CanonicalizePath(&path, &slash_bits, &path_err)) return DecorateError(clump->file_, target->diag_pos_, path_err, err); Node* node = state_->LookupNodeAtPos(path, target->pos_.dfs_location()); if (node == nullptr) { 
    return DecorateError(clump->file_, target->diag_pos_, "unknown target '" + path + "'", err); } state_->AddDefault(node); } } } // 将所有的Edge添加到全局的Edge vector容器中(*State->edges_),并对其分配id { 
    METRIC_RECORD(".ninja load : build edge table"); size_t old_size = state_->edges_.size(); size_t new_size = old_size; for (Clump* clump : clumps) { 
    new_size += clump->edges_.size(); } state_->edges_.reserve(new_size); for (Clump* clump : clumps) { 
    std::copy(clump->edges_.begin(), clump->edges_.end(), std::back_inserter(state_->edges_)); } // Assign edge IDs. ParallelMap(thread_pool_, IntegralRange<size_t>(old_size, new_size), [this](size_t idx) { 
    state_->edges_[idx]->id_ = idx; }); } return true; } 
(1) edge setup

构造输入/输出节点的初始图。选择一个可能保持碰撞次数较低的初始大小。Edge的非隐式输出的数量对于最终的节点的数量是一个足够好的代理。
执行FinishAddingClumpToGraph() -> AddEdgeToGraph()按照顺序,查询Rule -> 判断Pool-> 重置容器容量 -> 循环构建Node
构建完成,根据配置进行一些设置,此部分的内容就完成了

(2) link edge outputs
(3) link edge inputs

此时所有的重复Edge已经被剔除掉了,现在开始给input添加需要自己的edge

(4) default targets

添加默认的target

(5) build edge table

将所有的Edge添加到全局的Edge vector容器中(*State->edges_),并对其分配id

4 加载日志文件

会加载两个日志文件命名分别为.ninja_log.ninja_deps
.ninja_log保存ninja运行期间的所有日志
.ninja_deps保存了ninja的构建图,在此过程将节点添加到构建图中,查找每个节点的最后记录记录输出,并计算开发记录的总数.(使用ninja -t deps读取)

5 执行编译

在这里插入图片描述

subproc.Start()中使用posix_spawn()创建一个新的进程,使用/bin/sh -c "command"来执行编译指令

总结

两个特点:

  1. 由其他高级编译系统生成其输入文件
    1. ninja自带的ninja_syntax.py
    2. 安卓soong编译系统中的工具
    3. cmake -Gninja 生成ninja文件
  2. 更快的编译
    1. 编译前会构建一张图
    2. 根据图找到依赖关系链
    3. 根据依赖关系链,展开编译命令,构建子进程进行编译以提高编译速度

使用拓展

自带工具集

ninja自身集成了 graphviz 等一些对开发有用的工具,可以使用 ninja -t list 查看

讯享网ninja subtools: browse # 在浏览器中浏览依赖关系图。(默认会在 8080 端口启动一个基于python的http服务) clean # 清除构建生成的文件 commands # 罗列重新构建制定目标所需的所有命令 deps # 显示存储在deps日志中的依赖关系 graph # 为指定目标生成 graphviz dot 文件。 如 ninja -t graph all |dot -Tpng -o graph.png inputs # 显示目标的所有(递归)输入 path # 查找两个目标之间的依赖关系路径 paths # 查找两个目标之间的所有依赖项路径 query # 显示一个路径的inputs/outputs targets # 通过DAG中rule或depth罗列target compdb # dump JSON兼容的数据库到标准输出 recompact # 重新紧凑化ninja内部数据结构 

在这里插入图片描述

ninja -t graph ninja | dot -Tpng -o ninja.png 

使用ninja提高编译速度

讯享网source build/envsetup.sh lunch xxx make 

从Android O开始,soong已经是google的入口。从soong入口后,会经soong_ui,soong,kati,blueprint几个阶段,把mk,bp转换成ninja文件后,然后执行ninja命令解析ninja文件进行编译。
在这里插入图片描述

从图中来看,准备过程十分冗长,每次编译都是需要重新收集相关文件,重新编译成build.ninja,再合并为combined-xxx.ninja文件
如果我们舍弃掉准备的过程那么就可以直接指向ninja以提高速率
添加函数到 envsetup.sh
修改miui/build/envsetup.sh,新增一个quickbuild函数

function quickbuild() { 
    # 备份当前目录 local current=$PWD local out_dir="$ANDROID_BUILD_TOP/out" local file=$out_dir/combined-$TARGET_PRODUCT.ninja if [ ! -f $file ]; then file=$out_dir/combined-$TARGET_PRODUCT-target-files-package.ninja fi if [ -f $file ]; then echo "ninja: $file" $* else echo "ninja: $file not exist" return fi croot && prebuilts/build-tools/linux-x86/bin/ninja -f $file $* cd $current } 

注意 : 此方式只适用于修改c,cpp,java文件等,如果添加文件或修改配置文件,需要重新make生成ninja文件

小讯
上一篇 2025-03-14 09:52
下一篇 2025-02-13 23:55

相关推荐

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/24088.html