Anki 的 Ninja 构建系统实战指南:从 Bazel 迁移到./run与./ninja的完整工作流
【免费下载链接】ankiAnki is a smart spaced repetition flashcard program项目地址: https://gitcode.com/GitHub_Trending/an/anki
本文基于仓库 docs/ninja.md(其源文件为 docs-site/developers/ninja.mdx)展开,面向熟悉旧 Bazel 构建体系的开发者,系统讲解 Anki 当前的 Ninja 驱动构建系统:如何准备环境、用
./run一键构建启动、用./ninja运行分层测试与格式检查,并结合仓库源码剖析runner入口、目标分层机制与常见问题排查。读完本文,你将能独立完成 Anki 源码从环境准备、构建运行到测试、格式化、静态检查的完整开发闭环,并理解其构建系统的内部工作原理。
一、背景:Anki 为什么从 Bazel 走向 Ninja
Anki 是一个以 Rust 为核心(rslib/)、Python 为胶水层(pylib/、qt/aqt/)、TypeScript/Svelte 为前端(ts/)的三层大型项目。历史上其构建体系基于 Bazel,而当前仓库已经切换到Ninja(及其高性能替代 n2)驱动的构建方案。docs/ninja.md开篇即点明其定位:
Brief notes for people used to the existing Bazel build system.
也就是说,这份文档是写给"已熟悉 Bazel 构建系统"的开发者的迁移速记。与其对应,docs/development.md 中的"Building from source"一节给出了更完整的依赖要求:除了 Rustup 之外,需要N2 或 Ninja(1.10+),其中 n2 的构建状态输出更好,可用仓库自带的tools/install-n2一键安装;just命令运行器则作为实验性的官方命令入口被引入(见 justfile)。
整套构建的产物统一落入仓库根目录下的out/文件夹(Windows 上另有node_modules),Cargo、yarn、pip 的依赖缓存则复用系统共享缓存。理解了这套布局,再往下看具体的命令就水到渠成了。
二、环境准备:三步从零进入 Ninja 世界
按照docs/ninja.md的说明,从一个 Bazel 时代的旧 checkout 切换到 Ninja 构建体系需要完成三步:
- 把 ninja 二进制加入 PATH:文档明确指向 ninja v1.11.1 的发布版本。特别提醒:在 Windows 上,如果你同时在 msys 中安装了 ninja,必须确保原生(native)二进制在 PATH 中排在更前面,否则 msys 版本可能破坏构建行为。这一点在源码中有直接印证——build/runner/src/build.rs 构造子进程 PATH 时,在 Windows 上显式拼接了
out\bin;out\extracted\node;node_modules\.bin;...;\msys64\usr\bin,将构建所需工具链置于 msys 之前。 - 通过 rustup 安装 Rust:仓库根目录的 rust-toolchain.toml 固定了工具链版本
1.97.1,并声明了rust-analyzer组件;rustup 会在首次构建时自动下载该版本。注释明确说明:过旧的 Rust 版本可能根本无法编译,而过新的版本可能无法通过 clippy 测试,因此不要随意改动这个文件。 - 清理旧 checkout 残留:删除已有的
.bazel和node_modules文件夹。当前构建体系已将 Node 运行时、依赖安装全部托管进out/(例如out/extracted/node、:node_modules构建组),仓库根目录不再需要 Bazel 时代的这些遗留物。
另外,虽然文档推荐标准 ninja,但仓库实际上优先使用 n2:build/runner/src/build.rs中的get_ninja_command()(build.rs)会先探测 PATH 上是否存在n2,存在则用它,否则回退到ninja。安装 n2 只需执行仓库自带的 tools/install-n2(其内部通过 cargo 从 n2 上游安装并固定到指定 revision)。docs/development.md还提示:在 Windows 上若 WSL 与 MSYS2 bash 冲突导致安装报错,可改用C:\msys64\usr\bin\bash.exe tools/install-n2。
三、一键构建并启动:./run
环境就绪后,开发期启动 Anki 的方式非常简单:
./run # Windows 上为 .\rundocs/ninja.md对这一命令的描述只有一句话,但它背后的链路值得拆解。根目录的 run 脚本(bash)做了这几件事:
- 预设开发环境变量:
ANKIDEV=1(打印额外日志、禁用自动备份,切勿在正式 profile 上使用)、QTWEBENGINE_REMOTE_DEBUGGING=8080、ANKI_API_PORT=40000(可通过http://localhost:40000/_anki/pages/xxx.html直接访问前端页面,配合tools/web-watch可实现自动重建热调试); - 调用
./ninja pylib qt,把 Python 库与 Qt 界面编译到out/; - 最后用
out/pyenv/bin/python tools/run.py启动 Anki。
第一次构建会较慢(需要下载并编译大量 Rust 依赖、提取 Node、建立 Python 虚拟环境),之后即为增量构建。若需优化构建(运行更快、编译更慢),按 docs/development.md 的说明使用:
./tools/runopt # 等价于 RELEASE=1 ./run # 或 RELEASE=1 ./runRELEASE=2会做进一步优化但构建显著变慢。tools/runopt的实现也正是RELEASE=1 $(dirname $0)/../run(见 tools/runopt)。注意RELEASE、CI、MAC_X86、LIN_ARM64、SOURCEMAP、HMR等环境变量被写入RECONFIGURE_KEY(见 run 与 ninja),一旦这些值发生变化,构建系统会自动触发重新配置(reconfigure)。
四、./ninja入口:runner 与 build.ninja 的生成
./ninja是整个构建体系的核心入口,Windows 上对应tools\ninja。先看它的实现(ninja 脚本):
export CARGO_TARGET_DIR=$out/rust cargo build -p runner --profile $runner_profile # 编译构建器本身 exec $out/rust/$runner_profile/runner build -- $* # 把参数转交给 runnerrunner是一个位于 build/runner/src/main.rs 的 Rust 小工具,通过 clap 提供pyenv、yarn、rsync、run、build、archive六个子命令,负责跨平台地调用各种构建动作并静默成功输出。Windows 版本 tools/ninja.bat 逻辑一致,并特意将"构建 runner"与"运行 runner"拆成两步,避免构建环境变量泄漏到子进程。
build子命令的核心行为(build/runner/src/build.rs)非常值得一提:
- 自动引导:若
out/build.ninja不存在,会先执行cargo run -p configure(对应build/configurecrate)生成它(build.rs); - 快速失败重试:如果构建在 3 秒内失败,大概率是
build.ninja引用了被改名/删除的文件,系统会重新生成build.ninja并重试一次(build.rs); - 冒号转换:Ninja 目标名无法包含冒号,runner 会把
foo:bar自动改写成foo_bar(build.rs)——这正是一切分层目标的底层前提; - 友好输出:默认设置
NINJA_STATUS(如[%f/%t; %r active; %es]),强制开启颜色,成功时以绿色加粗打印Build succeeded in x.xx s.,失败则红字退出(build.rs)。
所以文档中./ninja check之类的命令,实际流程是:编译 runner → 生成/校验out/build.ninja→ 调用ninja(或n2)执行对应目标。
五、核心目标:check / format / fix
docs/ninja.md给出了三个最高频的目标,这里结合 docs/development.md 与源码把它们的覆盖范围讲透。
5.1./ninja check—— 跑全部测试与检查
./ninja check # Linux/macOS tools\ninja check # Windows它会一次性执行仓库所有检查目标。当前通过build/configure各模块注册的检查目标(可在对应源码中逐一核对)包括:
| 目标 | 覆盖内容 | 源码位置 |
|---|---|---|
check:pytest:pylib | pylib 的 pytest 测试 | build/configure/src/pylib.rs |
check:pytest:aqt | qt/aqt 的 pytest 测试 | build/configure/src/aqt.rs |
check:pytest:tools | tools 的 pytest 测试 | build/configure/src/python.rs |
check:mypy | Python 类型检查 | build/configure/src/python.rs |
check:ruff | Python lint | build/configure/src/python.rs |
check:clippy | Rust lint | build/configure/src/rust.rs |
check:rust_test | Rust 单元测试 | build/configure/src/rust.rs |
check:vitest | TypeScript/Svelte 单元测试 | build/configure/src/web.rs |
check:svelte | Svelte 类型检查(svelte-check) | build/configure/src/web.rs |
check:eslint | 前端 lint(--max-warnings=0,零警告容忍) | build/configure/src/web.rs |
check:typescript:aqt | qt 遗留 JS 的类型检查 | build/configure/src/aqt.rs |
check:minilints | 版权头、贡献者、许可证等小检查 | build/configure/src/rust.rs |
此外还有格式类检查目标:check:format:rust(rustfmt)、check:format:dprint、check:format:prettier、check:format:sql、check:format:python:{group}、check:format:proto、check:format:cog:{group}等,它们分布在 build/configure/src/rust.rs、build/configure/src/web.rs、build/ninja_gen/src/python.rs 等处。
5.2./ninja format—— 自动修正格式
./ninja format当check报告格式问题后,执行该命令即可自动修复。它对应上述各format:*目标(如format:rust、format:dprint、format:prettier、format:sql、format:python:{group}、format:proto、format:cog:{group})。以 Python 为例,build/ninja_gen/src/python.rs中PythonFormat的实际命令是$ruff format $mode $in && $ruff check --select I --fix $in,即先用 ruff 格式化,再自动修复 import 排序。
5.3./ninja fix—— 修复 eslint 与版权问题
./ninja fixfix面向两类问题:一是fix:eslint(对ts与qt/aqt/data/web/js两个目录运行带--fix的 eslint,见 build/configure/src/web.rs);二是fix:minilints(同步版权、贡献者、许可证声明,见 build/configure/src/rust.rs)。对应地,docs/development.md还建议 Rust 侧的 clippy 问题可用cargo clippy --fix处理。
5.4 只跑单个检查
docs/development.md给出了精细化复跑的范例:如果check输出中check:svelte:editor失败,可以只跑./ninja check:svelte:editor,或退一级用./ninja check:svelte重跑全部 Svelte 检查——这正是分层目标(下一节)的直接应用。justfile中的just test、just test-rust、just test-py、just test-ts、just lint、just fmt等命令最终也都映射到这些./ninja目标上(见 justfile),二者等价。
六、层次化目标(Hierarchical Targets)的实现原理
docs/ninja.md用两个例子说明了分层目标的用法:
./ninja check:jest:deck-options # 只跑 ts/deck-options 的 Jest 测试 ./ninja check:jest # 跑全部 Jest 测试也就是说,目标名用冒号分隔,越靠前越宽泛,越靠后越具体。这套机制有两层实现支撑:
第一层:分组树。build/ninja_gen/src/build.rs中的split_groups()(build.rs)把形如foo:bar:baz的目标逐级拆分为["foo:bar:baz", "foo:bar", "foo"],注册到以组为键的哈希表中;其单元测试(build.rs)明确断言了split_groups("foo:bar:baz") == ["foo:bar:baz", "foo:bar", "foo"]。因此运行最具体的叶子目标时会自动覆盖其全部祖先组,而运行宽泛组时则聚合其下所有子组产物。
第二层:冒号转义。由于 Ninja 自身无法在目标名中表达冒号,runner 在执行前将foo:bar统一改写为foo_bar(见上文 build.rs),从而把这套分层语法映射为 Ninja 可识别的扁平目标。
一个需要留意的历史差异:docs/ninja.md中示例写的是check:jest:deck-options,但当前仓库的前端测试框架已从 Jest 迁移到Vitest——package.json 中定义的是"vitest:once": "cd ts && vitest run",build/configure/src/web.rs 注册的也是check:vitest目标,测试文件位于ts/routes/deck-options等目录。也就是说:分层目标的使用原则完全不变,只是把示例中的jest换成当前的vitest(或直接使用你关心的具体子组名)。这一差异也解释了为何docs/ninja.md文件头部明确标注 "DO NOT MANUALLY EDIT THIS FILE"——它是从 docs-site/developers/ninja.mdx 自动同步的,更新应改源文件。
七、常见问题与排查路径
结合docs/ninja.md与 docs/development.md,开发中最常遇到的几类问题及对策如下:
n2 and ninja missing/failed. did you forget 'bash tools/install-n2'?:这是 runner 找不到任何 ninja 实现时的直接报错(build.rs)。按提示执行bash tools/install-n2(Windows 上为bash tools\install-n2)或把 ninja v1.11.1 加入 PATH 即可。- 构建刚启动就失败:大概率是
build.ninja引用了已被改名/删除的文件,runner 会自动重新生成并重试一次;若持续失败,可删除out/build.ninja强制重新引导。 - Windows 上 ninja 行为异常:检查 PATH 中是否混入了 msys/WSL 的版本,确保原生二进制排在前面(对应文档中的特别提醒);WSL 与 MSYS2 bash 冲突时,用完整路径调用 msys 的 bash 执行安装脚本。
- 误改工具链版本:
rust-toolchain.toml固定了 1.97.1,删除它使用发行版 Rust 时,较新版本通常能编译但可能挂测试,较旧版本可能根本无法编译(见 docs/development.md)。 - 想要干净重来:大部分构建产物都在
out/目录(Windows 另含node_modules),删除它即可完全重建、释放空间;Cargo/yarn/pip 的依赖缓存在系统共享位置(~/.cargo、~/.cache/yarn等),不受影响。 - 自定义目录加入构建而免于格式检查:把个人文件放进
extra/文件夹,会自动被版本跟踪与各类检查忽略(见 docs/development.md)。 - 开发与日常使用混用:开发时建议用
-p [profile名]加载独立 profile,因为ANKIDEV=1会禁用自动备份(见 run 与 docs/development.md)。
八、小结:一条命令打通整个开发循环
docs/ninja.md虽短,却勾勒出了 Anki 现代开发工作流的全部关键动作:
| 场景 | 命令 |
|---|---|
| 构建并运行 | ./run(Windows:.\run) |
| 优化模式运行 | RELEASE=1 ./run或./tools/runopt |
| 全部测试与检查 | ./ninja check |
| 单个/子组检查 | ./ninja check:svelte:editor、./ninja check:vitest等 |
| 修正格式 | ./ninja format |
| 修复 eslint/版权问题 | ./ninja fix |
| 修复 clippy | cargo clippy --fix |
其背后是"Rust runner + 生成的 build.ninja + 分层目标"三位一体的设计:runner 负责跨平台调用与冒号转义,build/configure与build/ninja_gen两个 crate 负责声明目标和依赖图,Ninja/n2 负责增量执行。对于从 Bazel 迁移过来的开发者,只需要记住"先装 ninja/n2 与 rustup、清掉旧构建残留,然后一切操作都围绕./run与./ninja展开"即可。更深入的构建体系细节,可继续阅读 docs/development.md 与 justfile 中的命令清单。
【免费下载链接】ankiAnki is a smart spaced repetition flashcard program项目地址: https://gitcode.com/GitHub_Trending/an/anki
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考