使用 Bazel 构建 Angular 框架:贡献者本地构建、测试与调试完整指南
本指南面向 Angular 框架本身的开发者(即在 angular 仓库内工作、修改
packages/源码的贡献者),它不是面向使用 Bazel 构建 Angular 应用的应用层教程。阅读本文后,你将掌握:Angular 仓库如何通过 npm 分发的 Bazelisk 驱动 Bazel 构建,如何执行单包/全仓构建与测试,如何用--config=debug、_debug目标完成 Node/Karma 测试的断点调试,以及如何借助 Stamping、远程缓存和 profile 分析工具诊断与加速大型 monorepo 的构建过程。文中所有命令与结论均以当前仓库(Bazel 版本见 .bazelversion)的真实配置为准。
为什么 Angular 要用 Bazel 构建自己
Bazel 是 Google 开源的高性能构建工具,核心卖点是快速、可靠的增量构建与可复现的构建结果。Angular 框架的大部分代码都由 Bazel 构建——从 MODULE.bazel 声明的依赖(rules_nodejs、aspect_rules_ts、aspect_rules_js、rules_angular、rules_esbuild 等)到 packages/core/BUILD.bazel 里的 ng_project、ng_package 目标,构建系统贯穿整个仓库。
把 Bazel 引入框架自举(self-host)构建有几个直接收益:
- 内容寻址缓存:Bazel 为所有 action 的输入计算内容哈希并作为缓存键,hermetic(密封)的构建可以跳过未变化的 action;
- 并行与远程执行:依赖关系被建模为图,可安全并行调度,甚至把 action 分发到远程执行服务;
- 统一跨语言构建:TypeScript、Sass、HTML 模板、浏览器测试、npm 打包在同一个构建图内描述。
因此,理解 Bazel 的工作流是向 Angular 提交代码前的必修课。
Bazelisk:通过 npm 固定 Bazel 版本
与其他项目要求开发者自行安装 Bazel 二进制不同,Angular 从 npm 安装 Bazel,而不是让贡献者直接安装。这样做的目的是保证所有贡献者使用完全相同的 Bazel 版本,避免"本地能过、CI 过不了"的版本漂移问题。
Bazel 的二进制由 npm 包 @bazel/bazelisk 及其平台相关依赖提供(Bazelisk 是 Bazel 的版本管理启动器)。在仓库根 package.json 的 devDependencies 中可以看到:
"@bazel/bazelisk": "^1.7.5",
"@bazel/ibazel": "0.28.0",
"@bazel/buildifier": "^8.0.0"
其中:
@bazel/bazelisk:提供bazel/bazelisk可执行入口,负责按 .bazelversion 中声明的版本(当前为8.7.0)下载并缓存对应 Bazel 发行版;@bazel/ibazel:Bazel 的 watch 模式启动器,用于文件变更后自动重建/重测;@bazel/buildifier:BUILD 文件格式化工具,用于保证.bzl/BUILD.bazel风格统一。
在仓库中运行 Bazel 的统一入口是 pnpm bazel 命令。该命令通过 pnpm 解析 node_modules/.bin 下的 Bazelisk 启动器来执行 Bazel,从而保证与仓库锁定的版本一致:
pnpm bazel --version
同理,仓库根 package.json 中的 test 脚本也直接定义为 bazelisk test,因此 pnpm test <target> 与 pnpm bazel test <target> 效果等价。
工作区骨架:从 WORKSPACE 到 MODULE.bazel
Bazel 通过根目录下的工作区文件声明"这是一个 Bazel 项目",并描述外部依赖的规则版本。**注意:**本文所依据的早期文档描述的是 WORKSPACE 文件,并提到构建规则来源于 npm_bazel_typescript(rules_typescript);而在当前仓库中,该文件已不存在,Bazel 工作区改由 Bzlmod 时代的两份文件描述:
- MODULE.bazel:声明模块名
angular,并以bazel_dep+git_override引入rules_pkg、rules_nodejs、aspect_rules_ts、aspect_rules_js、rules_angular、devinfra、rules_sass、rules_browsers等;同时通过 extension 固定 Node.js(node_version = "24.20.0")、pnpm 与 TypeScript(ts_version = "6.0.3")工具链; - REPO.bazel:声明哪些目录不应被 Bazel 扫描(
.git、dist、**/node_modules/**)。
此外 .bazelversion 固定 Bazel 本身版本(8.7.0),它正是 Bazelisk 选取发行版的依据。
.bazelrc:默认 flags 的实际面貌
Bazel 接受大量命令行选项,Angular 把团队统一的选项签入到根目录的 .bazelrc。这份文件是理解仓库构建行为的金钥匙,值得逐段精读。几个与日常开发强相关的片段:
符号链接前缀:如果不希望 Bazel 在工作目录下生成一堆 bazel-* 符号链接,可以在 .bazelrc 中加 build --symlink_prefix=/。而 Angular 仓库实际采用 dist/ 前缀(.bazelrc):
build --symlink_prefix=dist/
因此构建产物会以 dist/bin、dist/testlogs、dist/genfiles 等形式出现在工作区内,bazel-out 则被指向 dist/bin 等目录。该注释同时提醒:bazel-out 应当被编辑器排除(例如在 .vscode/settings.json 中忽略),历史上曾因 VSCode 递归扫描该巨型目录而在 macOS 触发 C++ 工具链自动发现的怪异失败(对应 Bazel issue #4603)。
调试测试用的 --config=debug(.bazelrc)是后续调试章节的核心,它在 test:debug 配置名下集中设置:
test:debug --test_arg=--node_options=--inspect-brk --test_output=streamed --test_strategy=exclusive --test_timeout=9999 --nocache_test_results --strategy=TestRunner=standalone
即:以 --inspect-brk 启动 Node 并挂起等待调试器、流式输出测试日志、独占执行且不超时、跳过缓存结果——这正是单测断点调试所需的全部开关。
输出模式(.bazelrc):query --output=label_kind 让 bazel query 打印如 ng_module rule //foo:bar 这样更可读的结果;test --test_output=errors 让失败的测试默认只打印错误日志。
文件末尾通过 try-import %workspace%/.bazelrc.user 加载用户个人配置,且必须是最后一个语句,以便用户配置可以覆盖仓库默认 flags——远程缓存等个人级开关就放在这里。
构建 Angular
构建某单个 npm 包,例如 @angular/core:
pnpm bazel build packages/core
构建全部包:
pnpm bazel build packages/...
packages/core 的目标由 packages/core/BUILD.bazel 定义:ng_project(name = "core") 聚合 src/**/*.ts 源码与 rxjs、zone.js、@angular/compiler 等依赖;ng_package 则负责产出可发布的 npm 包(含 package.json 替换、nested_packages 等),构建产物可进一步用于 integration/ 下的真实项目验证。一个关键实践:该文件中的依赖注释还强调"不要把依赖都塞进 ng_package",因为对完整 npm_package 的依赖会拖长重建时间。
如果希望边改代码边看到输出更新,可使用 ibazel(Bazel watch 模式):
pnpm ibazel build packages/core
ibazel 会监听源码变更并持续保持输出最新,适合本地迭代。
测试 Angular
仓库的测试目标也遵循 BUILD 图。常用的三组命令:
# 在 Node 中测试某个包
pnpm test packages/core/test:test
# 在 Karma(真实浏览器环境)中测试某个包
pnpm test packages/core/test:test_web
# 测试所有包
pnpm test packages/...
test 脚本在 package.json 中定义为 bazelisk test,因此这里的 pnpm test ... 等价于 pnpm bazel test ...。
关键语法说明:上面示例中的 ... 不是让你替换成包名的占位符,而是 Bazel 的通配符,表示"在该路径下递归匹配所有目标"。例如 packages/core/test/BUILD.bazel 中 ng_web_test_suite 目标定义了 karma 测试;若只想跑某个包的全部测试,应写成 // 开头的标签形式:
# 跑 packages/core 下的所有测试
pnpm test //packages/core/...
# 跑某一个具体的测试套件
pnpm test //packages/core/test:test
其中 //packages/core/test:test 是 target label(// + 包路径 + :target 名)。在仓库内多个 BUILD 文件中可以看到测试规则家族:angular_jasmine_test、js_test、zoneless_jasmine_test(无 Zone.js 场景)以及 ng_web_test_suite,它们统一由 tools/defaults.bzl 导出。Bazel 对构建结果有非常高效的缓存,因此首次构建某个目标通常明显慢于后续构建——之后的构建会直接命中缓存。
测试相关的常用 flags
当出现"看似无关的测试集体失败"时,往往是没有给 Bazel 测试跑搭配正确的 flags:
| Flag | 作用 |
|---|---|
--config=debug |
以调试模式构建与启动(细节见下文"调试"章节) |
--test_arg=--node_options=--inspect=9228 |
更改调试器监听端口(默认 9229) |
--test_tag_filters=<tag> |
按 tag 过滤测试,标签定义在对应 BUILD 文件的规则 tag 配置里 |
例如 .bazelrc 中默认 test --flaky_test_attempts=1(.bazelrc)保证本地不自动重试,CI 上才可能通过环境变量开启去 flaky 的三次尝试。
调试测试的三条路线
1. 用 Chrome DevTools 调试 Node 测试
--config=debug 会以 --inspect-brk 启动 Node,使进程在入口处挂起等待调试器:
- 打开 Chrome 访问
chrome://inspect; - 点击 Open dedicated DevTools for Node 启动独立 Node 调试器;
- 以 debug 配置运行测试,例如:
pnpm bazel test packages/core/test:test --config=debug
进程会自动连接到调试器。随后:
- 点击 Resume script execution(继续执行),让代码运行到第一个
debugger语句或已设断点处; - 若要检查生成的模板指令(Ivy 编译产物),可在调用栈中找到组件模板,点击代码底部的
(source mapped from [CompName].js);也可以在 DevTools 中关闭 sourcemap,或在 Sources 面板进入ng://命名空间查看全部生成代码。
2. 用 VSCode 调试 Node 测试
首次需要配置 launch.json:菜单 Debug > Add configuration 打开 launch.json,在 configurations 数组中加入:
{
"name": "Attach to Process",
"type": "node",
"request": "attach",
"port": 9229,
"restart": true,
"timeout": 600000,
"sourceMaps": true,
"skipFiles": ["<node_internals>/**"],
"sourceMapPathOverrides": {
"?:*/bin/*": "${workspaceFolder}/*"
},
"resolveSourceMapLocations": ["!**/node_modules/**"]
}
最简单的调试姿势:在代码里插入 debugger 语句或打断点,然后用 debug 配置启动对应 Bazel 测试:
pnpm bazel test <target> --config=debug
Bazel 会等待连接。切到调试视图(侧边栏,或 Mac 上 Apple+Shift+D),点击配置名旁的绿色播放按钮(即 Attach to Process)即可附着调试器。配置中的关键参数说明:
timeout: 600000:最长等待连接 10 分钟,覆盖 Bazel 冷启动/编译耗时;restart: true:进程重启后自动重连;sourceMaps/sourceMapPathOverrides:把 Bazel 产物目录中的源码映射回工作区原始 TS 文件,保证断点落在手写源码上。
3. 调试 Karma(浏览器)测试
Karma 测试运行在真实浏览器里,调试方式不同:给目标名追加 _debug 后缀即可。每个 ng_web_test_suite 目标都会自动生成一个额外的 _debug 目标,例如:
pnpm bazel run packages/core/test:test_web_debug
然后:
- 用任意浏览器打开
http://localhost:9876/debug.html; - 打开浏览器 DevTools 进行调试——可通过
fit/fdescribe聚焦特定用例,或在用例中插入debugger语句。
4. 调试 Bazel 规则本身
当问题出在 Bazel 规则而非测试代码时,可以查看 external 目录(包含 Bazel 执行工作区时下载的全部依赖):
open $(pnpm -s bazel info output_base)/external
bazel info output_base 返回 Bazel 的实际输出根目录。若要查看 Bazel 具体执行的子命令(常用于诊断规则内部行为):
pnpm bazel build //packages/core:package -s
-s(--subcommands)会打印每个 action 展开后的真实命令。
Stamping:把版本信息注入构建产物
Stamping 是 Bazel 的一项能力:允许在构建产物中嵌入来自版本控制系统等非 hermetic(非密封)信息(如 commit SHA、版本号)。
Angular 对 stamping 的配置方式如下(当前仓库的实际落点):
- 在 .bazelrc 中,把
workspace_status_command指向版本信息生成命令——release 与 snapshot 构建各有一份:
build:release --workspace_status_command="pnpm --silent ng-dev release build-env-stamp --mode=release"
build:release --stamp
build:snapshot-build --workspace_status_command="pnpm --silent ng-dev release build-env-stamp --mode=snapshot"
build:snapshot-build --stamp
- Bazel 在需要 stamp 二进制时会运行该脚本,读取其输出的
STABLE_*键值对。
需要说明的是:文档早期版本描述的实现文件是 tools/bazel_stamp_vars.js(在仓库内执行 git 命令生成版本信息),当前仓库中已找不到该文件——从 .bazelrc 的现状推断,这部分逻辑已被整合进 @angular/ng-dev 的 ng-dev release build-env-stamp 命令。当前 package.json 的 devDependencies 中引用的正是 @angular/ng-dev。
实际使用中,跑 release/snapshot 构建时带上对应 config 即可自动获得带版本信息的产物。另外 .bazelrc 中的 build:snapshot-build-firefox --nostamp 是一个有趣的边界案例:Mozilla 要求 Firefox 版 Angular DevTools 提交源码并由 Mozilla 复现构建,而 stamping 依赖 .git 目录会导致上传体积超限,因此该变体显式关闭 stamping。
Remote cache:让干净构建也能命中缓存
Bazel 支持从远端缓存拉取 action 结果,从而使即使在本机没有历史缓存的情况下(如 CI 的干净环境)也能复用先前的构建产物。
原理是:Bazel 给所有 action 输入计算基于内容的哈希,该哈希用作 action 输出的缓存键。由于 Bazel 构建是 hermetic 的,只要输入哈希在缓存中存在,就可以直接跳过 action 执行。
重要的前提约束:一旦启用缓存,非 hermetic 的 action 会带来严重问题——最坏情况下你会从缓存取回损坏的产物,使构建不可复现。因此 Angular 的实现原则是:所有 Bazel 规则只依赖其声明的输入,绝不直接读写文件系统或底层环境。
当前 Angular 只在 CI 上默认使用远程缓存,核心开发者可以手动开启以加速本地构建。
开发环境中启用远程缓存
说明:这一小节适用于具备 Google 内部账号权限的开发者(原文注明 only available to Googlers),普通贡献者按 CI 流程即可。
启用步骤(基于当前 .bazelrc 中已有的 remote-cache 配置):
- 进入 service account 页面,选择内部项目;
- 选择 Angular local dev,点击 Edit,滚动到底部点击 Create key;
- 弹出窗口中选择 JSON 作为 Key type,点击 Create;
- 将密钥保存在安全位置;
- 在工作区根目录创建
.bazelrc.user文件,写入:
build --config=angular-team --google_credentials=[ABSOLUTE_PATH_TO_SERVICE_KEY]
.bazelrc 中的对应基础配置(build:remote-cache)已经指向 Google Cloud Storage 上的团队缓存桶并通过 google_default_credentials 认证:
build:remote-cache --remote_cache=https://storage.googleapis.com/angular-team-cache
build:remote-cache --remote_accept_cached=true
build:remote-cache --remote_upload_local_results=false
build:remote-cache --google_default_credentials
注意 --remote_upload_local_results=false:本地开发默认只读团队缓存,避免把未经验证的本地产物污染共享缓存;build:trusted-build --remote_upload_local_results=true 则用于需要上传结果的受信构建。
诊断慢构建:从 profile 到火焰图式分析
构建变慢时,先产出一份 profile,再做量化分析。
第一步,用 --profile 记录构建过程:
pnpm bazel build //packages/compiler --profile filename_name.profile
这会生成 filename_name.profile 文件,可用 chrome://tracing 或 Bazel 自带的 analyze-profile 命令分析。
控制台报告
直接在终端获得简明报告:
pnpm bazel analyze-profile filename_name.profile
输出包含 phase summary(阶段汇总)、各 phase 详情与 critical path(关键路径)。
进一步列出每个任务及其耗时,--task_tree 接受一个正则来过滤任务文本:
pnpm bazel analyze-profile filename_name.profile --task_tree ".*"
只显示超过某阈值的任务用 --task_tree_threshold,默认阈值为 50ms:
pnpm bazel analyze-profile filename_name.profile --task_tree ".*" --task_tree_threshold 5000
例如 TypeScript 编译在 profile 中的任务文本形如:
70569 ACTION_EXECUTE (10974.826 ms) Compiling TypeScript (devmode) //packages/compiler:compiler []
要筛出所有耗时超过 5 秒的 TypeScript 编译任务:
pnpm bazel analyze-profile filename_name.profile --task_tree "Compiling TypeScript" --task_tree_threshold 5000
HTML 报告
可视化更强的 HTML 报告:
pnpm bazel analyze-profile filename_name.profile --html --html_details --html_histograms
这会生成 filename_name.profile.html,用浏览器打开即可。界面右上角有一个小型目录,链接到三个区域:
- Tasks:时间花销的关系图,悬停背景显示所处 phase,悬停色块显示对应 action 的详情;
- Legend:Tasks 图中颜色含义图例;
- Statistics:每个 phase 的耗时及内部时间分配,通常 execution phase 最长,并附关键路径信息。
Statistics 区还包含 Skylark(Starlark 加载阶段)统计,分为 User-Defined 与 Builtin 两类函数执行时间。点击 "self" 表头两次可按函数自耗时(ms)降序排列。
诊断建议:聚焦所有 phase 与函数中时间开销最大的少数项目——通常单个(或同类多个)条目占据了压倒性耗时,找到它即可定位优化点。
已知问题与平台避坑
Windows
bazel run 报 MODULE_NOT_FOUND:在 Windows 的 Bazel 上 bazel run 仅对非测试目标有效,对测试目标会抛出类似 Cannot find module '...runfiles...' 的 MODULE_NOT_FOUND 错误。解决方式是改用 bazel test:
pnpm bazel test packages/core/test/bundling/forms:symbol_test
mkdir 缺失:若出现 fetch 阶段 mkdir -p _ failed: 错误,说明缺少 msys64 及其工具(如 mkdir),而构建 Angular 需要它们。确认 C:\msys64\usr\bin 位于系统(System)PATH 而非用户(User)PATH;之后执行一次 git clean -xfd,再依次运行 pnpm 与 pnpm build 通常可恢复。
macOS / Xcode
pnpm bazel build packages/... 若返回 Xcode version must be specified to use an Apple CROSSTOOL(apple_cc_toolchain / @local_config_cc 解析失败),可能与 VSCode 的交互有关。若关闭 VSCode 即可修复,可在 VSCode 配置中加入:
"files.exclude": {"bazel-*": true}
(把 Bazel 生成的目录树排除出文件监视,避免编辑器打开海量文件句柄。)
若 VSCode 并非根因,可尝试按顺序执行:
# 退出 VSCode(确保没有 VSCode 进程在运行)
bazel clean --expunge
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
sudo xcodebuild -license
pnpm bazel build //packages/core # 先在 VSCode 外跑一次构建以预热 Xcode 工具链
快速命令速查表
| 目标 | 命令 |
|---|---|
| 构建单个包 | pnpm bazel build packages/core |
| 构建全部包 | pnpm bazel build packages/... |
| Watch 模式 | pnpm ibazel build packages/core |
| Node 测试某包 | pnpm test packages/core/test:test |
| Karma 测试某包 | pnpm test packages/core/test:test_web |
| 跑某目录全部测试 | pnpm test //packages/core/... |
| 调试 Node 测试 | pnpm bazel test <target> --config=debug |
| 调试 Karma 测试 | pnpm bazel run <target>:test_web_debug(浏览器打开 localhost:9876/debug.html) |
| 查看外部依赖 | pnpm -s bazel info output_base |
| 慢构建分析 | pnpm bazel build <target> --profile p.profile 后接 analyze-profile 各子命令 |
| 查看规则子命令 | pnpm bazel build <target> -s |
需要提醒的是:由于仓库本身在持续演进(Bzlmod 迁移、规则集升级、stamping 实现迁移到 ng-dev),文中标注的 .bazelrc/.bazelversion/package.json 快照以当前仓库为准;若命令行为与本文不符,请先核对上述配置文件是否已更新。理解这份配置骨架后,你就能在本地自由地构建、测试并调试 Angular 框架本身了。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00