基础设施 4.0 · 优秀 2026-08-27 · 文章

Bazel Module Versions Aren't SemVer

packages.ecosyste.ms 把 Bazel Central Registry 上 protobuf 最新版显示成四年前的 3.19.6,根因是用严格 SemVer 解析器统一过滤所有生态:protobuf 自 2022 年去掉 3....

打开原文回到归档

Bazel Module Versions Aren't SemVer

中文导读

packages.ecosyste.ms 把 Bazel Central Registry 上 protobuf 最新版显示成四年前的 3.19.6,根因是用严格 SemVer 解析器统一过滤所有生态:protobuf 自 2022 年去掉 3. 前缀后是两段的 36.0,.bcr.1 补丁是五段含字母,全部解析失败被当不稳定版本。文章拆解 bzlmod 的 Version.java 语法:release 段数任意、段内可含字母,按 SemVer 预发布规则逐段比较;.bcr.N 是注册表补丁约定——36.0 因 macOS 工具链完整性问题被 yank 后,修复版以追加段而非 +build 发布,为的是在 Minimal Version Selection 下严格大于 36.0,否则所有人被钉死在坏版本。+build 元数据在解析期即被剥离,空字符串版本排最顶表示本地覆盖永远赢 MVS。vers 跨生态规范尚未覆盖 Bazel,修复是把 Version.java 排序移植为 bazel scheme。

原文(抓取存档 · AK-RSS source archive (crawled 2026-08-30))

# Bazel Module Versions Aren’t SemVer
> 作者: Andrew Nesbitt
> 发布时间: 2026-08-27T10:00:00+00:00
> 原文链接: https://nesbitt.io/2026/08/27/bazel-module-versions-arent-semver.html

---

Aman Sharma [reported](https://github.com/git-pkgs/enrichment/issues/65) this week that [packages.ecosyste.ms](https://packages.ecosyste.ms/) had the latest version of protobuf on the [Bazel Central Registry](https://registry.bazel.build/) (BCR, the default package index for Bazel’s built-in dependency manager) as `3.19.6`. The [current release](https://registry.bazel.build/modules/protobuf) is `36.0.bcr.1`. Protobuf [dropped the leading `3.` in 2022](https://protobuf.dev/support/version-support/) and has shipped `21.x` through `36.x` since, so `3.19.6` is four years behind.

The [root cause](https://github.com/ecosyste-ms/packages/issues/1810) is that the “latest release” query filters out anything the [`semantic`](https://rubygems.org/gems/semantic) gem fails to parse. That code predates the [vers](https://github.com/andrew/vers) library and I’ve yet to swap it over, so it’s still applying one strict SemVer parser to every ecosystem. SemVer requires exactly three numeric segments before the hyphen. `36.0` has two, `0.7.1.bcr.1` has five, and `bcr` contains letters, so every protobuf release since `3.19.6` fails the parse and gets treated as unstable. The same filter reports [glog](https://registry.bazel.build/modules/glog)’s latest as `0.7.1` when the registry has `0.7.1.bcr.1`.

Bazel is a build tool first, and its module system ([bzlmod](https://bazel.build/external/overview#bzlmod), the default since Bazel 7) is mostly used to pull in existing C++, Java and Go projects that already have their own release histories. Rather than force those projects to renumber, Bazel [documents](https://bazel.build/external/module#version-format) its version format as a deliberate loosening of SemVer: any SemVer string is valid and sorts the same way, and beyond that the release part can have any number of segments and each segment can contain letters. Abseil’s date-based `20210324.2` and protobuf’s two-segment `36.0` are both accepted as-is. The grammar in [`Version.java`](https://github.com/bazelbuild/bazel/blob/a8a2c0c1fa063a0bba4e7222a1b1d2c8a4e0e632/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/Version.java#L33-L48) is `RELEASE[-PRERELEASE][+BUILD]`, where `RELEASE` is dot-separated identifiers of ASCII letters and digits only. Comparison applies SemVer’s [prerelease identifier rules](https://semver.org/#spec-item-11) to the release segments as well:

-   numeric identifiers sort numerically, so `36.0` > `4.0.0`
-   any identifier containing a letter sorts above every purely numeric one, then as an ASCII string
-   a version with more release segments sorts above one that shares its prefix, so `0.7.1.bcr.1` > `0.7.1`
-   a prerelease sorts below the same release part on its own, so `36.0-rc2` < `36.0`

The `.bcr.N` suffix is a [registry convention](https://github.com/bazelbuild/bazel-central-registry/blob/main/docs/README.md#module-versions) rather than part of the format. Registry entries are immutable once published, and the registry often carries small patches on top of the upstream tarball to make a project build cleanly under Bazel. When one of those patches needs updating and the upstream source stays as-is, the fixed entry is published under a new version with `.bcr.1` tacked onto the release part. That’s why `36.0.bcr.1` exists: `36.0` was [yanked](https://github.com/bazelbuild/bazel-central-registry/blob/main/modules/protobuf/metadata.json) for a macOS toolchain integrity mismatch, and the fix went in as extra release segments rather than a `+build` tag so that it sorts strictly above `36.0`. That ordering matters because Bazel resolves dependencies with [Minimal Version Selection](https://research.swtch.com/vgo-mvs), the same algorithm as Go modules, which always picks the highest version any dependent asked for; a suffix that sorted equal or lower would leave everyone pinned to the broken build.

`Version.java` also handles build metadata differently from SemVer §10: rather than being ignored for comparison and kept, the `+BUILD` part is [stripped at parse time](https://github.com/bazelbuild/bazel/blob/a8a2c0c1fa063a0bba4e7222a1b1d2c8a4e0e632/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/Version.java#L69-L72) before anything is stored or sent to a registry, so `1.2.3+abc` and `1.2.3` are equal and interchangeable rather than equal-but-distinct. The empty string is a valid version and [sorts higher than everything else](https://github.com/bazelbuild/bazel/blob/a8a2c0c1fa063a0bba4e7222a1b1d2c8a4e0e632/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/Version.java#L182-L186); it marks a module that’s been overridden to point at a local directory or git commit instead of a registry release, and sorting it top means a local override always wins MVS. Since the release segments have no fixed meaning, breaking changes were originally signalled by a separate `compatibility_level` integer in the module file, roughly equivalent to a SemVer major version. Both active release lines [made that field a no-op](https://github.com/bazelbuild/bazel/pull/28615) in February 2026 (9.1.0, backported to 8.6.0) after it caused resolution failures only the module authors could resolve, leaving breaking changes to be reported by the module’s own build-time errors instead.

External parsers get `.bcr.N` wrong one of two ways:

-   strict SemVer (the `semantic` gem): rejects `0.7.1.bcr.1` because the release part must be exactly three integers, so it drops from the stable set
-   permissive generic (the vers fallback): accepts the extra segments, applies prerelease semantics to the alphabetic `bcr`, and sorts it below `0.7.1`

The [vers spec](https://github.com/package-url/vers-spec), the package-url project’s cross-ecosystem notation for version ranges, enumerates comparison schemes for [fifteen ecosystems](https://github.com/package-url/vers-spec/blob/main/docs/types/vers-types.md) and has yet to cover Bazel, though `pkg:bazel` is a [registered purl type](https://github.com/package-url/purl-spec/blob/main/types/bazel-definition.json) so there is already a slot for a `vers:bazel` scheme. Both vers libraries I maintain hit the second case. The fix in [Ruby](https://github.com/andrew/vers/pull/34) and [Go](https://github.com/git-pkgs/vers/pull/43) is a port of the `Version.java` sort as an implementation-defined `bazel` scheme ahead of the spec. The corresponding [ecosyste.ms change](https://github.com/ecosyste-ms/packages/pull/1811) keeps `.bcr.N` releases eligible as stable, lets the yanked flag exclude `36.0`, and moves both protobuf and glog to the correct latest version.

[](https://nesbitt.io/2026/08/27/bazel-module-versions-arent-semver.html)