首页
/ 使用 Bazel 构建 Angular 框架:贡献者本地构建、测试与调试完整指南

使用 Bazel 构建 Angular 框架:贡献者本地构建、测试与调试完整指南

2026-09-07 19:40:43作者:董灵辛Dennis

本指南面向 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_nodejsaspect_rules_tsaspect_rules_jsrules_angularrules_esbuild 等)到 packages/core/BUILD.bazel 里的 ng_projectng_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.jsondevDependencies 中可以看到:

"@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_pkgrules_nodejsaspect_rules_tsaspect_rules_jsrules_angulardevinfrarules_sassrules_browsers 等;同时通过 extension 固定 Node.js(node_version = "24.20.0")、pnpm 与 TypeScript(ts_version = "6.0.3")工具链;
  • REPO.bazel:声明哪些目录不应被 Bazel 扫描(.gitdist**/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/bindist/testlogsdist/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_kindbazel 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 源码与 rxjszone.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.bazelng_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_testjs_testzoneless_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,使进程在入口处挂起等待调试器:

  1. 打开 Chrome 访问 chrome://inspect
  2. 点击 Open dedicated DevTools for Node 启动独立 Node 调试器;
  3. 以 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

然后:

  1. 用任意浏览器打开 http://localhost:9876/debug.html
  2. 打开浏览器 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 的配置方式如下(当前仓库的实际落点):

  1. .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
  1. Bazel 在需要 stamp 二进制时会运行该脚本,读取其输出的 STABLE_* 键值对。

需要说明的是:文档早期版本描述的实现文件是 tools/bazel_stamp_vars.js(在仓库内执行 git 命令生成版本信息),当前仓库中已找不到该文件——从 .bazelrc 的现状推断,这部分逻辑已被整合进 @angular/ng-devng-dev release build-env-stamp 命令。当前 package.jsondevDependencies 中引用的正是 @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 配置):

  1. 进入 service account 页面,选择内部项目;
  2. 选择 Angular local dev,点击 Edit,滚动到底部点击 Create key
  3. 弹出窗口中选择 JSON 作为 Key type,点击 Create
  4. 将密钥保存在安全位置;
  5. 在工作区根目录创建 .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,再依次运行 pnpmpnpm build 通常可恢复。

macOS / Xcode

pnpm bazel build packages/... 若返回 Xcode version must be specified to use an Apple CROSSTOOLapple_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 框架本身了。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
916
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388