构建执行流程:从 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 Planbuild.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::AddTargetAddSubTargetbuild.cc:95)从目标节点逆着输入方向递归遍历:

  • 无 in_edge 的叶子若是脏的且非 dep-loader 生成,说明缺源文件 → 报 “missing and no known rule to make it”(build.cc:102)。
  • 边默认以 kWantNothingwant_;若该节点脏且 want 仍是 nothing,则升为 kWantToStart 并调 EdgeWantedbuild.cc:133)。
  • 然后递归该边的所有 inputs_

EdgeWantedbuild.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 不预先算出一个拓扑序列,而是维护一个动态的”就绪集合”。ComputeCriticalPathbuild.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 入队

FindWorkbuild.cc:162)从 ready_ 弹出优先级最高的边(jobserver 模式下还要先抢到 token,见 文档 8)。ScheduleWorkbuild.cc:180)把边从 kWantToStart 推进 kWantToFinish:若其 pool ShouldDelayEdge() 则交给 pool 延迟,否则直接 ready_.push

3. Builder:主循环

Builder::Buildbuild.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 占用的变化能即时反映到节流上。StartEdgebuild.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 Nconfig_.parallelism,在 CanRunMore 里限流。
  • Pool → ScheduleWork/ScheduleInitialEdges 把超额边延迟,根本不进 ready_
  • 两者叠加:先被 pool 卡的边连排队的机会都没有,过了 pool 这关再受 -j 总量约束。

4. CommandRunner:启动与回收

CommandRunner 是个纯虚接口(build.h:154):CanRunMoreStartCommandWaitForCommandGetActiveEdgesAbort。这层抽象让测试能注入 mock(见 代码结构 §5),也让 -n dry-run 能换成 DryRunCommandRunnerbuild.cc:50)。

真实实现 RealCommandRunnerreal_command_runner.cc:21)的关键成员:

SubprocessSet subprocs_;                          // real_command_runner.cc:41  在跑的子进程集
std::map<const Subprocess*, Edge*> subproc_to_edge_;  // :43  子进程 → 边 的反查表
  • CanRunMorereal_command_runner.cc:60)——容量 = parallelism - (running+finished),再受 -l 负载收紧;为保证有进度,容量为 0 但无进程在跑时强制为 1。
  • StartCommandreal_command_runner.cc:89)——edge->EvaluateCommand() 求出命令字符串,subprocs_.Add(command, edge->use_console()) 起子进程,登记进 subproc_to_edge_
  • WaitForCommandOrJobserverTokenreal_command_runner.cc:103)——循环 subprocs_.DoWork() 直到有事件。子进程完成时(SubprocFinished),取出退出码和输出,subproc_to_edge_ 反查回边并删除映射,打包成 BuildResult::CommandCompleted(edge, status, output) 返回。这就是”完成的子进程映射回边”的关键。

5. FinishCommand:命令完成后的回路

Builder::FinishCommandbuild.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::EdgeFinishedbuild.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 抽取(ExtractDepsbuild.cc:1003)和日志写入的细节分别见 依赖发现增量构建

6. 顶层入口 RunBuild

NinjaMain::RunBuildninja.cc:1633)把上述一切串起来:

  1. CollectTargetsFromArgsninja.cc:384)——无参数时用 state_.DefaultNodes(),否则解析每个参数为目标。
  2. 构造 Builder,对每个目标 builder.AddTargetninja.cc:1655)。AddTarget 返回 false 且 err 为空表示”已是最新”,不算错误。
  3. builder.AlreadyUpToDate(),打印 “no work to do.” 并返回(ninja.cc:1670)。
  4. 否则 builder.Build()ninja.cc:1677),按返回码决定退出状态(”interrupted by user” → ExitInterrupted)。

7. 进度显示

Status 是纯虚接口(status.h:27),构建在几个关键点回调它,进度条得以更新:

回调 触发点
EdgeAddedToPlan EdgeWantedbuild.cc:158)——确定总数
BuildStarted Build 开始(build.cc:716
BuildEdgeStarted StartEdgebuild.cc:849
BuildEdgeFinished FinishCommandbuild.cc:926
BuildFinished 各退出路径

实现 StatusPrinterstatus_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 怎么判断”哪些边其实需要建”。


本站访问量

This site uses Just the Docs, a documentation theme for Jekyll.