ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Bazel 模块扩展(Module Extensions)完全指南:从 `use_extension` 到 `extension_metadata` 的实战与原理

2026/9/13 6:56:19 拓冰建站 浏览量
Bazel 模块扩展(Module Extensions)完全指南:从 `use_extension` 到 `extension_metadata` 的实战与原理 Bazel 模块扩展Module Extensions完全指南从use_extension到extension_metadata的实战与原理【免费下载链接】bazela fast, scalable, multi-language and extensible build system项目地址: https://gitcode.com/GitHub_Trending/ba/bazel模块扩展Module Extensions是 Bazel 模块系统Bzlmod中最强大的扩展点它允许你读取整个依赖图中各模块声明的数据tags执行解析逻辑最终通过调用仓库规则repo rules生成外部仓库从而在尊重 Bazel 模块依赖图的前提下与 Maven、Go modules、npm 等外部包管理系统对接。本文以官方文档 docs/external/extension.mdx 为骨架结合本仓库源码系统讲解如何定义、使用模块扩展以及如何利用extension_metadata、facts、override_repo/inject_repo等机制写出可维护、可复现的扩展。读完后你将能独立编写一个生产级的模块扩展并理解其底层执行与命名规则。什么是模块扩展模块扩展允许用户扩展 Bazel 的模块系统它读取来自依赖图中各模块的输入数据执行必要的解析逻辑最后通过调用仓库规则见 docs/external/repo.mdx创建仓库。模块扩展与仓库规则能力相当——都可以进行文件 I/O、发送网络请求等其中最重要的能力是让 Bazel 与其他包管理系统交互同时仍然尊重由 Bazel 模块构建出来的依赖图。几个关键设计点模块扩展定义在.bzl文件中与仓库规则类似扩展不会被直接调用相反每个模块通过声明**标签tags**来为扩展提供数据Bazel 在评估任何扩展之前先完成模块解析module resolution扩展读取的是整个依赖图中所有属于它的标签而不只是当前模块的标签。从源码结构看模块扩展的实现集中在 src/main/java/com/google/devtools/build/lib/bazel/bzlmod 下的ModuleExtension、TagClass等类中而面向用户的 Starlark 接口则在 RepositoryModuleApi.java 中定义并在 StarlarkRepositoryModule.java 中实现。这一划分也印证了文档所述module_extension与repository_rule共享同一套 Starlark 宿主机制。使用模块扩展Extension usage模块扩展本身托管在 Bazel 模块中。要在某个模块中使用扩展需要两步先用bazel_dep声明对托管该扩展的模块的依赖再调用内置函数use_extension把它引入当前作用域。下面是从MODULE.bazel文件中使用rules_jvm_external模块内 maven 扩展的示例bazel_dep(name rules_jvm_external, version 4.5) maven use_extension(rules_jvm_external//:extensions.bzl, maven)use_extension的签名参见 docs/versions/9.1.0/rules/lib/globals/module.mdx为module_extension_proxy use_extension(extension_bzl_file, extension_name, *, dev_dependencyFalse, isolateFalse)参数说明extension_bzl_file必填定义模块扩展的 Starlark 文件标签extension_name必填要使用的模块扩展名称该符号必须由上述 Starlark 文件导出dev_dependency默认False若为True当当前模块不是根模块或启用了--ignore_dev_dependency时本次扩展使用将被忽略isolate默认False实验性参数需配合--experimental_isolated_extension_usages使用启用后该扩展使用将与所有其他使用相互隔离把use_extension的返回值绑定到变量后就可以用点语法为扩展指定标签。标签必须符合扩展定义中对应tag class标签类所规定的 schema。例如指定maven.install和maven.artifact标签maven.install(artifacts [org.junit:junit:4.13.2]) maven.artifact(group com.google.guava, artifact guava, version 27.0-jre, exclusions [com.google.j2objc:j2objc-annotations])扩展生成的仓库是其 API 的一部分。使用use_repo指令将这些仓库引入当前模块作用域use_repo(maven, maven)use_repo的签名支持两种导入方式docs/versions/9.1.0/rules/lib/globals/module.mdxNone use_repo(extension_proxy, *args, **kwargs)args要导入的仓库名列表kwargs以不同名字导入仓库键为当前作用域要用的名字值为扩展导出的原始名字。非合法标识符的键可以通过字面 dict 传参指定例如use_repo(extension_proxy, **{foo.2: foo})。以上面的声明为例maven 扩展承诺生成一个名为maven的仓库于是maven//:org_junit_junit这样的标签就能被正确解析到该扩展生成的仓库。注意延迟求值。模块扩展是惰性求值的通常只有当某个模块用use_repo引入了扩展的某个仓库、并且该仓库在构建中被引用时扩展才会被求值。测试扩展时bazel mod deps会无条件求值所有模块扩展非常有用。定义模块扩展Extension definition定义模块扩展与定义仓库规则docs/external/repo.mdx类似使用module_extension函数。不同的是仓库规则有若干属性attributes而模块扩展拥有若干tag class标签类每个 tag class 又有一组属性。tag class 定义了该扩展所用标签的 schema。例如上文 maven 扩展的定义# rules_jvm_external//:extensions.bzl _install tag_class(attrs {artifacts: attr.string_list(), ...}) _artifact tag_class(attrs {group: attr.string(), artifact: attr.string(), ...}) maven module_extension( implementation _maven_impl, tag_classes {install: _install, artifact: _artifact}, )tag_class由tag_class(attrs{}, docNone)创建attrs声明该标签类的全部属性映射关系为属性名到属性对象见 RepositoryModuleApi.java 中的tag_class与TagClassApi。module_extension函数的完整签名RepositoryModuleApi.javamodule_extension(implementation, tag_classes{}, docNone, environ[], os_dependentFalse, arch_dependentFalse, facts_version0)implementation必填实现函数接收单个参数module_ctx在构建开始时调用一次以确定可用仓库集合tag_classes声明扩展使用的所有标签类映射标签类名到tag_class对象doc可被文档生成工具提取的扩展描述environ已弃用迁移到module_ctx.getenvos_dependent/arch_dependent标记扩展是否依赖操作系统 / 架构facts_versionfactsdict 的 schema 版本号持久化在 lockfile 中与当前值不一致时丢弃已持久化的 facts。module_extension的实现函数与仓库规则实现函数相似区别在于它拿到的是一个module_ctx对象该对象允许访问所有使用此扩展的模块及其相关标签实现函数随后调用仓库规则来生成仓库# rules_jvm_external//:extensions.bzl load(bazel_tools//tools/build_defs/repo:http.bzl, http_file) # a repo rule def _maven_impl(ctx): # 以下仅为演示用途的伪实现 # 收集来自整个依赖图的 artifacts artifacts [] for mod in ctx.modules: for install in mod.tags.install: artifacts install.artifacts artifacts [_to_artifact(artifact) for artifact in mod.tags.artifact] # 调用 coursier CLI 工具解析依赖 output ctx.execute([coursier, resolve, artifacts]) repo_attrs _process_coursier_output(output) # 调用仓库规则生成仓库 for attrs in repo_attrs: http_file(**attrs) _generate_hub_repo(name maven, repo_attrs)module_ctx的关键字段包括docs/versions/9.1.0/rules/lib/builtins/module_ctx.mdxmodules依赖图中所有使用本扩展的 Bazel 模块列表每个都是bazel_module对象暴露其为本扩展指定的所有标签迭代顺序保证为从根模块开始的广度优先搜索顺序os/arch访问宿主系统与架构信息的 structgetenv(name, defaultNone)读取环境变量值变化会触发重新求值facts上一次执行时通过extension_metadata的facts参数返回的 dictextension_metadata(...)向 Bazel 提供关于扩展所生成仓库的元数据详见后文file/extract/execute/download等与repository_ctx类似的文件与网络操作。本仓库的测试 ModuleExtensionResolutionTest.java 中simpleExtension、multipleModules等用例正是对多个模块向同一扩展投递标签、扩展聚合生成仓库这一流程的端到端验证。扩展身份Extension identity模块扩展由use_extension调用中出现的名称与.bzl文件共同标识。例如下面示例中扩展maven由.bzl文件rules_jvm_external//:extensions.bzl与名称maven标识maven use_extension(rules_jvm_external//:extensions.bzl, maven)从不同的.bzl文件重新导出同一扩展会赋予它一个新身份如果两个版本的扩展同时出现在传递模块图中它们会被分别求值且各自只能看到属于该特定身份的标签。因此作为扩展作者你应确保用户只会从单一.bzl文件使用你的模块扩展——移动扩展文件属于破坏性的公共 API 变更。仓库命名与可见性Repository names and visibility扩展生成的仓库的规范名称形如module_repo_canonical_nameextension_namerepo_name注意规范名称格式不是可依赖的 API随时可能变化。这一命名策略带来两个推论每个扩展拥有自己的仓库命名空间两个不同扩展可以各自定义同名仓库而互不冲突repository_ctx.name返回的是仓库的规范名称并非仓库规则调用中指定的名称。将模块扩展生成的仓库纳入考虑后仓库可见性规则如下一个 Bazel 模块仓库可以看到其MODULE.bazel文件中通过bazel_dep与use_repo引入的所有仓库模块扩展生成的仓库可以看到托管该扩展的模块可见的所有仓库加上同一模块扩展生成的其他所有仓库以仓库规则调用中指定的名字作为其 apparent name这可能导致冲突若模块仓库可见名为foo的仓库而扩展又生成了名为foo的仓库那么对于该扩展生成的所有仓库而言foo指的是前者类似地在模块扩展实现函数中扩展创建的仓库可以互相以 apparent name 在属性中引用与创建顺序无关若与模块可见的仓库冲突可将传给仓库规则属性的标签用Label包裹以确保其指向模块可见的仓库而非同名扩展生成仓库。这些规则在 ModuleExtensionResolutionTest.java 的generatedReposHaveCorrectMappings、generatedReposHaveCorrectMappings_internalRepoWins、generatedReposHaveCorrectMappings_strictDepsViolation等测试中有直接验证。覆盖与注入模块扩展仓库根模块可以使用override_repo与inject_repo来覆盖或注入模块扩展仓库。示例用 vendored 副本替换rules_java的java_tools# MODULE.bazel local_repository use_repo_rule(bazel_tools//tools/build_defs/repo:local.bzl, local_repository) local_repository( name my_java_tools, path vendor/java_tools, ) bazel_dep(name rules_java, version 7.11.1) java_toolchains use_extension(rules_java//java:extension.bzl, toolchains) override_repo(java_toolchains, remote_java_tools my_java_tools)override_repo将扩展生成的名为remote_java_tools的仓库替换为my_java_tools。use_repo_rule见 docs/versions/9.1.0/rules/lib/globals/module.mdx#use_repo_rule直接在MODULE.bazel中按仓库规则方式调用并创建仓库此类仓库仅对当前模块可见。示例修补一个 Go 依赖使其依赖zlib而非系统 zlib# MODULE.bazel bazel_dep(name gazelle, version 0.38.0) bazel_dep(name zlib, version 1.3.1.bcr.3) go_deps use_extension(gazelle//:extensions.bzl, go_deps) go_deps.from_file(go_mod //:go.mod) go_deps.module_override( patches [ //patches:my_module_zlib.patch, ], path example.com/my_module, ) use_repo(go_deps, ...) inject_repo(go_deps, zlib)# patches/my_module_zlib.patch --- a/BUILD.bazel b/BUILD.bazel -1,6 1,6 go_binary( name my_module, importpath example.com/my_module, srcs [my_module.go], - copts [-lz], cdeps [zlib], )inject_repo将zlib模块仓库注入到go_deps扩展的可见性范围中使该扩展生成的仓库能够引用zlib而无需依赖系统中的 zlib。最佳实践Best practices将每个扩展放在独立的文件中扩展位于不同文件时一个扩展可以加载另一个扩展生成的仓库。即使当前用不到该功能最好也放在独立文件里以备将来所需。原因在于扩展身份基于其所在文件日后移动文件会改变公共 API对用户而言是破坏性变更。声明可复现性并使用 facts如果你的扩展在相同输入扩展标签、读取的文件等下总是定义相同的仓库特别是不依赖任何没有校验和保护的下载见module_ctx.download那么考虑返回带reproducible True的extension_metadatareturn ctx.extension_metadata(reproducible True)reproducible True让 Bazel 在写入MODULE.bazellockfile 时跳过该扩展有助于保持 lockfile 精简、减少合并冲突的几率。注意Bazel 仍会以跨服务器重启持久化的方式缓存可复现扩展的结果因此即使扩展运行时间很长标记为可复现也不会带来性能损失。本仓库的 lockfile 测试 bazel_lockfile_test.py 中即包含标记reproducible True后扩展条目不出现在 lockfile 中的断言。如果你的扩展依赖构建外部获得的有效不可变数据最常见的是来自网络的数据但下载没有校验和保护可考虑用extension_metadata的facts参数持久化记录这些数据从而使扩展变得可复现return ctx.extension_metadata( reproducible True, facts {some_immutable_data: {...}}, )facts是键为字符串、值为任意 JSON 风格 Starlark 值的 dict总是持久化在 lockfile 中并通过module_ctx.facts字段提供给扩展后续的求值。facts的典型用法记录某个 SDK 各版本号到「下载 URL 校验和」对象的映射。第一次求值时从网络抓取该映射之后的求值直接使用facts中的数据以避免网络请求。需要注意的facts语义在 ModuleExtensionResolutionTest.java 的facts_supportedTypes、facts_unsupportedType以及 bazel_lockfile_test.py 中均有测试佐证facts不会因为模块扩展代码变化而失效因此要做好facts结构变化的处理Bazel 假定同一扩展两次求值产生的两个factsdict 可以浅合并如同对两个 dict 应用|运算符这也由module_ctx.facts不支持枚举条目、仅支持按键查找来部分强制若需要对facts中存储值的 schema 做破坏性变更将module_extension的facts_version参数设置为比之前更高的整数。Bazel 会把facts_version与 facts 一起持久化在 lockfile 中当记录的版本与当前值不一致时丢弃已持久化的 facts确保扩展只会观察到由相同 schema 产生的 facts见 RepositoryModuleApi.java。extension_metadata的完整签名docs/versions/9.1.0/rules/lib/builtins/module_ctx.mdx#extension_metadataextension_metadata module_ctx.extension_metadata(*, root_module_direct_depsNone, root_module_direct_dev_depsNone, reproducibleFalse, facts{})root_module_direct_deps/root_module_direct_dev_deps扩展认为属于根模块直接dev依赖的仓库名列表。若根模块通过use_repo额外导入了仓库或未导入全部这些仓库Bazel 求值时打印警告并提示运行bazel mod tidy自动修复use_repo调用。二者要么同时指定要么都不指定且列表必须不相交其中一方可以取特殊值all等价于列出扩展生成的全部仓库名reproducible声明扩展完全可复现因此不应存入 lockfilefacts提供给未来求值的 JSON 风格 dict。指明对操作系统与架构的依赖如果扩展依赖操作系统或其架构类型务必在扩展定义中用os_dependent和arch_dependent两个布尔属性标明。这样 Bazel 才能识别出当两者之一发生变化时需要重新求值。由于这种对宿主环境的依赖会让该扩展的 lockfile 条目更难维护如有可能考虑将其标记为可复现。只有根模块才能直接影响仓库名记住扩展创建仓库时它们创建在扩展的命名空间内。这意味着不同模块使用同一扩展并创建同名仓库时可能发生冲突——这通常表现为模块扩展的tag_class有一个name参数被直接作为仓库规则的name值传入。举例根模块A依赖模块B二者都依赖模块mylang。如果A和B都调用mylang.toolchain(namefoo)它们都会试图在mylang模块内创建名为foo的仓库于是报错。为避免这种情况要么去掉直接设置仓库名的能力要么只允许根模块这样做。允许根模块拥有该能力是 OK 的因为没有模块会依赖根模块它无需担心其他模块创建冲突的名字。延伸阅读模块扩展的定义与使用是 Bzlmod 的核心完整的模块系统背景见 docs/external/overview.mdx 与 docs/external/module.mdx扩展最终通过仓库规则创建仓库仓库规则的定义与重新拉取时机见 docs/external/repo.mdxMODULE.bazellockfile 与可复现性的关系见 docs/external/lockfile.mdxbazel mod系列命令含无条件求值所有扩展的bazel mod deps见 docs/external/mod-command.mdx。【免费下载链接】bazela fast, scalable, multi-language and extensible build system项目地址: https://gitcode.com/GitHub_Trending/ba/bazel创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考