Ninja 代码结构分析
本文回答两个问题:源码怎么组织,以及 Ninja 怎么构建它自己(含自举 bootstrap 与 re2c 代码生成)。建议先读 架构概述 建立全局印象。
1. 仓库顶层
ninja/
├── src/ 几乎全部源码(~30700 行 C++,含测试)
├── doc/ manual.asciidoc(官方手册)+ doxygen
├── misc/ ninja_syntax.py、bash/zsh 补全、工具脚本
├── windows/ Windows 资源文件
├── configure.py Python 生成器:产出 build.ninja(可自举)
├── CMakeLists.txt CMake 构建(与 configure.py 二选一)
├── README.md 构建说明
└── CONTRIBUTING.md / RELEASING.md / COPYING(Apache-2.0)
整个工具的代码量很小——约 3 万行(含全部单元测试与性能基准)。这种”小”本身就是设计的一部分:手册的非目标里明确拒绝内置规则、构建期条件判断等会让代码膨胀、让运行变慢的特性。
2. src/ 按职责分组
src/ 里文件很多(127 个),但按职责能干净地分成几组。注意一个命名约定:每个模块 foo.{h,cc} 通常都配一个 foo_test.cc 单元测试;带 -win32/-posix 后缀的是平台特定实现;.in.cc 是 re2c 的输入(见 §4)。
构建图模型 / 全局状态
- graph.h / graph.cc —
Node、Edge、DependencyScan、ImplicitDepLoader、EdgePriorityQueue - state.h / state.cc —
State(全局图)、Pool - eval_env.h / eval_env.cc —
Env、EvalString、Rule、BindingEnv - dyndep.h、dyndep_parser.h — 动态依赖
解析 / 词法
- lexer.h、lexer.in.cc(re2c 源)→ lexer.cc(生成)
- parser.h / parser.cc — 解析器基类
- manifest_parser.h / manifest_parser.cc
- depfile_parser.in.cc → depfile_parser.cc(生成)
- string_piece.h、string_piece_util.h — 零拷贝字符串
构建 / 执行
- build.h / build.cc —
Plan、Builder、CommandRunner接口、BuildConfig - real_command_runner.cc —
RealCommandRunner - build_result.h、command_collector.h
- exit_status.h、load_status.h、timestamp.h
日志 / 依赖
- build_log.h / build_log.cc —
BuildLog、BuildLogUser - deps_log.h / deps_log.cc —
DepsLog - missing_deps.h / missing_deps.cc
- clparser.h / clparser.cc — 解析 MSVC
/showIncludes
子进程 / 平台
- subprocess.h、subprocess-posix.cc、subprocess-win32.cc
- jobserver.h、jobserver-posix.cc、jobserver-win32.cc
- disk_interface.h / disk_interface.cc
- getopt.c(为 Windows/AIX 内置)、msvc_helper-win32.cc、minidump-win32.cc
工具类
- util.h / util.cc —
CanonicalizePath、错误打印、GetProcessorCount、GetLoadAverage - hash_map.h、metrics.h、edit_distance.h(拼写纠错)、elide_middle.h(状态行截断)、explanations.h(
-d explain)、json.h、line_printer.h、status.h
子命令工具(-t ...)
- browse.cc + browse.py —
-t browse网页 UI(Python 经 inline.sh 内联进二进制) - graphviz.cc —
-t graph输出 dot - clean.cc —
-t clean/-t cleandead - 其余工具(compdb、deps、query、targets、rules、recompact 等)直接内联在 ninja.cc 里
3. 入口与 NinjaMain
整个 CLI 都在 ninja.cc 里。main()(ninja.cc:1924)是唯一的外部符号,其余几乎都在匿名命名空间内。main 只是个薄封装,真正的驱动是 real_main(ninja.cc:1832)。
核心数据结构都挂在 struct NinjaMain : public BuildLogUser(ninja.cc:90)上,这样各个 -t 工具能直接访问:
State state_; // ninja.cc:102 构建图
RealDiskInterface disk_interface_; // ninja.cc:105 磁盘
BuildLog build_log_; // ninja.cc:110 .ninja_log
DepsLog deps_log_; // ninja.cc:111 .ninja_deps
子命令通过 ChooseTool(ninja.cc:1338)查一张静态表 kTools[]。每个 Tool 带一个 when 阶段枚举(RUN_AFTER_FLAGS / RUN_AFTER_LOAD / RUN_AFTER_LOGS,ninja.cc:210),real_main 在对应阶段调用它——例如 -t graph 需要先加载 manifest,所以是 RUN_AFTER_LOAD。选项解析在 ReadFlags(ninja.cc:1730),用 getopt_long,短选项串是 "d:f:j:k:l:nt:vw:C:h"(ninja.cc:1746)。
一个”速度优先”的小细节:
real_main顶部注释(ninja.cc:1833)说明它故意用exit()而不是return,”to avoid potentially expensive cleanup when destructing NinjaMain”——退出时干脆不析构那些大数据结构。
4. 构建系统:configure.py、CMake 与 re2c
Ninja 可以用两种方式构建(README.md:22):Python 生成器 configure.py,或 CMake。
4.1 configure.py:生成 build.ninja,并能自举
configure.py 用 misc/ninja_syntax.py 的 Writer 生成一份 build.ninja(configure.py:250)。它把核心源码编成两个对象库 libninja-re2c.a(configure.py:535)和 libninja.a(configure.py:588),再加 ninja.cc 的 main() 链成 ninja(configure.py:601)。
自举(--bootstrap) 解决”先有鸡还是先有蛋”:没有 ninja 怎么构建 ninja?class Bootstrap(configure.py:121)包住 Writer,在生成每条 build 边的同时直接执行对应的编译命令(_run_command,configure.py:196),于是不依赖任何已有的 ninja 就能产出一个 ./ninja;之后这个 ./ninja 再用刚写出的 build.ninja 重新构建自己(README.md:31:./configure.py --bootstrap)。
configure.py 还会生成一条自我再生成规则(configure.py:762):规则 configure 重跑 configure.py,且 build.ninja 依赖 configure.py 与 misc/ninja_syntax.py。所以你改了生成器脚本,下次 ninja 会先重新生成 build.ninja ——这正是 real_main 重建循环(架构概述 §4)所消费的能力。
4.2 re2c:生成词法器
Ninja 的两个扫描器——manifest 词法器和 depfile 解析器——不是手写的,而是用 re2c 从 .in.cc 里的正则/动作块生成 DFA。configure.py:513-518(configure.py:513)定义了 re2c 规则:
n.rule('re2c',
command='re2c -b -i --no-generation-date --no-version -o $out $in', ...)
n.build(src('depfile_parser.cc'), 're2c', src('depfile_parser.in.cc'))
n.build(src('lexer.cc'), 're2c', src('lexer.in.cc'))
生成出的 .cc 第一行就是 /* Generated by re2c */(lexer.cc:1)。重要的是:生成结果已经签入仓库,所以普通构建不需要装 re2c——只有当你修改了 .in.cc 语法时才需要它(configure.py:520 有相应警告)。读语法逻辑请看 .in.cc,.cc 只是把 DFA 状态机展开后的等价代码。
4.3 CMake
CMakeLists.txt 是等价的另一条路:同样 find_program(RE2C) 并在缺失时回退到签入的 .cc(CMakeLists.txt:63),同样建 libninja-re2c / libninja 两个 OBJECT 库与 ninja 可执行文件(CMakeLists.txt:208),要求 cxx_std_17(CMakeLists.txt:192)。
5. 测试与性能基准
测试基于 GoogleTest。入口 ninja_test.cc 只是 InitGoogleTest + RUN_ALL_TESTS(),所有 *_test.cc 链进同一个 ninja_test 二进制。
共享测试设施在 test.h:
StateTestWithBuiltinRules(test.h:30)——预置了一个cat规则的Statefixture。VirtualFileSystem : public DiskInterface(test.h:51)——内存文件系统 mock,Create()/Tick()让测试脚本化文件内容与 mtime。这是把构建/图逻辑与真实 IO 解耦的关键缝隙,配合CommandRunner接口(build.h:154)可被 mock,使整条构建主循环能在内存里被测。ScopedTempDir、ScopedFilePath(test.h:90)——RAII 清理真实临时文件。
测试按模块一一对应(最大的 build_test.cc 有约 4400 行,针对 VirtualFileSystem 跑整个 Builder/Plan 循环)。注意:单元测试只有在通过 --gtest-source-dir 提供 GoogleTest 源码时才会被编译(configure.py:621)。另有一组独立的性能基准 *_perftest.cc 和 hash_collision_bench.cc,不属于 ninja_test。
6. 小结:行号速查
| 主题 | 位置 |
|---|---|
入口 main / real_main | ninja.cc:1924 / ninja.cc:1832 |
NinjaMain 数据结构 | ninja.cc:90 |
子命令表 ChooseTool | ninja.cc:1338 |
选项解析 ReadFlags | ninja.cc:1730 |
自举 Bootstrap | configure.py:121 |
| 自我再生成规则 | configure.py:762 |
| re2c 规则 | configure.py:513 |
| 生成标记 | lexer.cc:1 |
| 测试入口 / VFS mock | ninja_test.cc:17 / test.h:51 |
下一篇 构建图模型 进入 Node/Edge/State 的数据结构细节。