构建执行流程:从 Plan 到命令执行
构建图 建好、脏检查 标完之后,就进入真正的执行阶段。这一阶段由三个角色协作:
Plan——决定”哪些边要建、按什么顺序就绪”。Builder——主循环,驱动”取就绪边 → 启动 → 等完成 → 解锁下游”。CommandRunner——抽象”启动一条命令、回收一条完成的命令”,真实实现是RealCommandRunner(底层是 子进程)。
1. 总览:调用链
RunBuild (ninja.cc:1633)
├─ CollectTargetsFromArgs (ninja.cc:384) 命令行参数 → 目标节点
├─ Builder.AddTarget (build.cc:668)
│ ├─ scan_.RecomputeDirty (graph.cc:49) 脏检查(见文档 6)
│ └─ plan_.AddTarget (build.cc:95) 图遍历,填 want_ 集合
└─ Builder.Build (build.cc:699) 主循环
├─ plan_.PrepareQueue (build.cc:588) 关键路径 + 初始就绪边
└─ while plan_.more_to_do():
├─ CanRunMore (real_command_runner.cc:60) -j/-l/池 限流
├─ plan_.FindWork (build.cc:162) 取最高优先级就绪边
├─ StartEdge (build.cc:841) → StartCommand 起子进程
├─ WaitForCommand… (real_command_runner.cc:103) 回收完成的子进程
└─ FinishCommand (build.cc:894) 抽 deps、restat、写日志、解锁下游
2. Plan:算出”要建什么”
class Plan(build.h:42,实现 build.cc:82)的核心数据:
std::map<Edge*, Want> want_; // build.h:134 从目标可达的每条边及其状态
EdgePriorityQueue ready_; // build.h:136 就绪队列(关键路径优先)
int command_edges_; // build.h:143 有命令的非 phony 边总数(进度分母)
int wanted_edges_; // build.h:146 剩余想建的边
Want 枚举(build.h:93)三态:
kWantNothing——这条边本身不必建(但可能要建它的下游)。kWantToStart——想建,尚未调度。kWantToFinish——已调度,等完成。
more_to_do()(build.h:55):wanted_edges_ > 0 && command_edges_ > 0。
2.1 AddTarget:图遍历填 want_
Plan::AddTarget → AddSubTarget(build.cc:95)从目标节点逆着输入方向递归遍历:
- 无 in_edge 的叶子若是脏的且非 dep-loader 生成,说明缺源文件 → 报 “missing and no known rule to make it”(build.cc:102)。
- 边默认以
kWantNothing入want_;若该节点脏且 want 仍是 nothing,则升为kWantToStart并调EdgeWanted(build.cc:133)。 - 然后递归该边的所有
inputs_。
EdgeWanted(build.cc:153)是计数与进度的源头:
void Plan::EdgeWanted(const Edge* edge) {
++wanted_edges_;
if (!edge->is_phony()) {
++command_edges_; // 进度条分母
if (builder_)
builder_->status_->EdgeAddedToPlan(edge);
}
}
2.2 就绪队列是动态的
Plan 不预先算出一个拓扑序列,而是维护一个动态的”就绪集合”。ComputeCriticalPath(build.cc:472)确实做了一次真正的拓扑排序,但只用来给每条边赋 critical_path_weight 优先级,不决定实际顺序。
实际顺序这样涌现:
PrepareQueue (build.cc:588)
├─ ComputeCriticalPath 赋优先级权重
└─ ScheduleInitialEdges (build.cc:559) 把初始 AllInputsReady 的边放进 ready_
边完成后:
EdgeFinished (build.cc:202)
→ NodeFinished (build.cc:234)
→ EdgeMaybeReady (build.cc:257) 若下游边 AllInputsReady 则 ScheduleWork 入队
FindWork(build.cc:162)从 ready_ 弹出优先级最高的边(jobserver 模式下还要先抢到 token,见 文档 8)。ScheduleWork(build.cc:180)把边从 kWantToStart 推进 kWantToFinish:若其 pool ShouldDelayEdge() 则交给 pool 延迟,否则直接 ready_.push。
3. Builder:主循环
Builder::Build(build.cc:699)是整个执行的心脏。结构上分”启动相”和”回收相”两段,在 while (plan_.more_to_do()) 里交替。
3.1 启动相
// build.cc:724-758(节选意译)
size_t capacity = command_runner_->CanRunMore(); // -j / -l / 池 共同决定容量
while (capacity > 0) {
Edge* edge = plan_.FindWork();
if (!edge) break; // 没有就绪边
if (edge 是 phony)
plan_.EdgeFinished(edge, kEdgeSucceeded, err); // 立即完成,无子进程
else {
StartEdge(edge, err); // 真正起命令
++pending_commands; --capacity;
capacity = command_runner_->CanRunMore(); // 重新查容量(负载可能变化)
}
}
每起一条边就重新查一次 CanRunMore,这样系统负载(-l)或 pool 占用的变化能即时反映到节流上。StartEdge(build.cc:841)会创建必要的输出目录、写 rspfile、记录 running_edges_ 起始时间,并调 command_runner_->StartCommand(edge)。
3.2 回收相
// build.cc:766-820(节选意译)
if (pending_commands) {
result = command_runner_->WaitForCommandOrJobserverToken(plan_.work_ready());
if (result.command_completed()) {
--pending_commands;
FinishCommand(&result, err); // 处理完成的命令
// 失败则 --failures_allowed
} else if (result.interrupted()) {
Cleanup(); return "interrupted by user";
} else if (result.jobserver_token_available()) {
continue; // 拿到 token,回去重试 FindWork
}
}
若既起不动新命令、又没有在跑的命令,但 more_to_do() 仍为真,说明卡住了——根据情况报 “subcommand(s) failed” / “cannot make progress” / “stuck [this is a bug]”(build.cc:822)。
3.3 并发由谁决定
-j N→config_.parallelism,在CanRunMore里限流。- Pool →
ScheduleWork/ScheduleInitialEdges把超额边延迟,根本不进ready_。 - 两者叠加:先被 pool 卡的边连排队的机会都没有,过了 pool 这关再受
-j总量约束。
4. CommandRunner:启动与回收
CommandRunner 是个纯虚接口(build.h:154):CanRunMore、StartCommand、WaitForCommand、GetActiveEdges、Abort。这层抽象让测试能注入 mock(见 代码结构 §5),也让 -n dry-run 能换成 DryRunCommandRunner(build.cc:50)。
真实实现 RealCommandRunner(real_command_runner.cc:21)的关键成员:
SubprocessSet subprocs_; // real_command_runner.cc:41 在跑的子进程集
std::map<const Subprocess*, Edge*> subproc_to_edge_; // :43 子进程 → 边 的反查表
CanRunMore(real_command_runner.cc:60)——容量 =parallelism - (running+finished),再受-l负载收紧;为保证有进度,容量为 0 但无进程在跑时强制为 1。StartCommand(real_command_runner.cc:89)——edge->EvaluateCommand()求出命令字符串,subprocs_.Add(command, edge->use_console())起子进程,登记进subproc_to_edge_。WaitForCommandOrJobserverToken(real_command_runner.cc:103)——循环subprocs_.DoWork()直到有事件。子进程完成时(SubprocFinished),取出退出码和输出,从subproc_to_edge_反查回边并删除映射,打包成BuildResult::CommandCompleted(edge, status, output)返回。这就是”完成的子进程映射回边”的关键。
5. FinishCommand:命令完成后的回路
Builder::FinishCommand(build.cc:894)在一条命令成功/失败后做一连串收尾,顺序很关键:
1. 抽取 deps (build.cc:905) 先做:从 depfile/showIncludes 提取头文件依赖
—— 抽取失败要让命令算失败
2. 计时收尾 (build.cc:920) 从 running_edges_ 查起始时间,算耗时
3. status 回调 (build.cc:926) BuildEdgeFinished
4. 失败分支 (build.cc:930) 失败则只 plan_.EdgeFinished(kEdgeFailed) 返回
5. restat (build.cc:934) 见文档 6:重新 stat 输出,内容没变则剪下游
6. plan_.EdgeFinished (build.cc:970) 标记输出就绪、解锁下游
7. 清 rspfile (build.cc:974)
8. 写 build log (build.cc:978) RecordCommand:命令哈希 + 起止时间 + mtime
9. 写 deps log (build.cc:987) RecordDeps:把抽取到的隐式依赖落盘
第 6 步 Plan::EdgeFinished(build.cc:202)是”解锁下游”的枢纽:
// 释放 pool 容量,放出被延迟的边
edge->pool()->EdgeFinished(*edge);
edge->pool()->RetrieveReadyEdges(&ready_);
// 释放 jobserver token(若有)
jobserver_->Release(std::move(edge->job_slot_));
// 成功:标输出就绪,沿出边传播
edge->outputs_ready_ = true;
for (each output) NodeFinished(output); // → EdgeMaybeReady 调度新就绪的下游
deps 抽取(ExtractDeps,build.cc:1003)和日志写入的细节分别见 依赖发现 和 增量构建。
6. 顶层入口 RunBuild
NinjaMain::RunBuild(ninja.cc:1633)把上述一切串起来:
CollectTargetsFromArgs(ninja.cc:384)——无参数时用state_.DefaultNodes(),否则解析每个参数为目标。- 构造
Builder,对每个目标builder.AddTarget(ninja.cc:1655)。AddTarget返回 false 且err为空表示”已是最新”,不算错误。 - 若
builder.AlreadyUpToDate(),打印 “no work to do.” 并返回(ninja.cc:1670)。 - 否则
builder.Build()(ninja.cc:1677),按返回码决定退出状态(”interrupted by user” →ExitInterrupted)。
7. 进度显示
Status 是纯虚接口(status.h:27),构建在几个关键点回调它,进度条得以更新:
| 回调 | 触发点 |
|---|---|
EdgeAddedToPlan | EdgeWanted(build.cc:158)——确定总数 |
BuildStarted | Build 开始(build.cc:716) |
BuildEdgeStarted | StartEdge(build.cc:849) |
BuildEdgeFinished | FinishCommand(build.cc:926) |
BuildFinished | 各退出路径 |
实现 StatusPrinter(status_printer.cc)维护 started_edges_/finished_edges_ 计数,按 --status / $NINJA_STATUS 格式打印那行熟悉的 [N/M] 进度,并预测 ETA。
8. 小结:行号速查
| 主题 | 位置 |
|---|---|
Plan / Want 枚举 | build.h:42 / build.h:93 |
AddSubTarget / EdgeWanted | build.cc:95 / build.cc:153 |
FindWork / ScheduleWork | build.cc:162 / build.cc:180 |
ComputeCriticalPath / PrepareQueue | build.cc:472 / build.cc:588 |
Builder::Build 主循环 | build.cc:699 |
CommandRunner 接口 | build.h:154 |
RealCommandRunner(反查表) | real_command_runner.cc:21(:43) |
FinishCommand | build.cc:894 |
Plan::EdgeFinished | build.cc:202 |
RunBuild | ninja.cc:1633 |
下一篇 增量构建 回到执行之前的关键一步——Ninja 怎么判断”哪些边其实需要建”。