Migrating to Bazel Modules (a.k.a. Bzlmod) - Fixing and Patching Breakages¶
In the previous Bzlmod post, we covered writing your own Bazel module extensions to adapt your own setup code for dependencies that aren't Bzlmod compatible. However, there are other Bzlmod incompatibilities and related breakages that module extensions alone can't fix, such as forbidden API usage or Windows path length errors.
This post shows you how to patch your dependencies and covers several situations where patching is the only solution. We'll describe how to create and apply patches for your dependencies, if you can't wait for upstream fixes (or contribute them yourself).
This article is part of the series "Migrating to Bazel Modules (a.k.a. Bzlmod)":
- Migrating to Bazel Modules (a.k.a. Bzlmod) - The Easy Parts
- Migrating to Bazel Modules (a.k.a. Bzlmod) - Repo Names and Runfiles
- Migrating to Bazel Modules (a.k.a. Bzlmod) - Repo Names and rules_pkg
- Migrating to Bazel Modules (a.k.a. Bzlmod) - Repo Names, Macros, and Variables
- Migrating to Bazel Modules (a.k.a. Bzlmod) - Module Extensions
- Migrating to Bazel Modules (a.k.a. Bzlmod) - Fixing and Patching Breakages
I occasionally update these blogs based on feedback, noting the changes in the Updates section at the bottom whenever I do. So don't forget to check the earlier blog posts every so often for new and improved information!
Prerequisites¶
As always, please acquaint yourself with the following concepts pertaining to external repositories if you've yet to do so:
As a quick refresher, here are two ways to generate a patch (both produce the same output format):
git diff
produces a patch with changes against the currentHEAD
.diff -urN a b
generates a patch between any two directoriesa
andb
.
Assuming a patch with the file name foo.patch
, patch -Np1 -i file.patch
applies the patch in the current directory.
Patches for problems module extensions can't solve¶
Continuing the story from the previous post, rules_scala
had other Bzlmod
incompatibility issues that couldn't be fixed with only a module extension. Many
of these were related to repo name handling. (I've since contributed fixes for
these Bzlmod breakages in #1621, #1650, and #1694, but they
aren't yet available in an official release.) We also had a problem with long
repository names in rules_rust
on Windows, and with a pre-Bzlmodified vendored
copy of protobuf
.
I'll share how I solved these deeper Bzlmod incompatibilty problems by generating my own patches for these external dependencies. Hopefully these changes inspire insights that can help solve the problems you face.
How to generate and apply a patch to a Bazel module or other repo¶
The basic process for creating patches and applying them to dependencies is:
git clone
the repository you wish to modify to your local machine.- Within the cloned repository,
git checkout
the tag corresponding to the version you wish to target with your patch. - Add a
MODULE.bazel
file to the cloned repository if it doesn't already have one. - In your project, immediately after the
bazel_dep()
declaring the dependency inMODULE.bazel
, uselocal_path_override()
to reference the cloned repository. - Update the cloned repository until your project builds successfully.
- Use
git diff
(or whatever method you prefer) within the cloned repository to generate a patch. - Save the patch file in your project's repository.
- Replace
local_path_override()
withsingle_version_override()
to reference the actual repository distribution and apply the patch.
For example, this is how I use local_path_override()
in EngFlow/example to find my local bazelbuild/rules_scala
repo:
Using local_path_override() in MODULE.bazel | |
---|---|
Here is how I use single_version_override()
to apply a patch to protobuf
in
the 'bzlmod-enable' branch of my mbland/rules_scala repo:
If the dependency does not have a MODULE.bazel
file, and you're not ready to
add one yourself, make the following adjustments:
- Replace
bazel_dep()
andlocal_path_override()
withlocal_repository()
(requires Bazel 7.2.0 or later). - Replace
single_version_override()
withhttp_archive()
(available in Bazel 6) or whichever repository rule applies.
If you have many dependencies to patch, you can also store your patches in a
custom Bazel registry and remove the local_path_override()
. Bazel will
automatically apply the patches from the registry. In general, however, it's
better if your repo shows that you've patched the dependency, and the patch is
committed to version control.
Use non-registry overrides or a custom registry for unpublished rules.
As Manuel Naranjo suggested in a #bzlmod thread in the Bazel Slack
workspace, archive_override()
can import Bazel modules that
aren't published to a registry. Other non-registry overrides, and
repository rules like local_repository()
that don't access the Bazel
Central Registry or other public server, can serve this purpose as
well.
Patches can produce a maintenance burden
Patch files can be difficult to maintain. They can conflict with upstream changes, requiring you to checkout the new version, apply the patch, resolve conflicts, test, and generate a new patch. It's better to upstream your patched changes if possible, then drop the patch!
Avoiding native.register_toolchain()
calls¶
Among the most common WORKSPACE
isms that break Bzlmod compatibility are macros
that call native.register_toolchain()
. Under Bzlmod, register_toolchain
can
only appear in the MODULE.bazel
file. Borrowing from the "Module extensions"
section of the previous post:
WORKSPACE |
MODULE.bazel |
---|---|
Allows native.register_toolchains() calls, including in *_toolchains() macros. The main repository's WORKSPACE file must contain calls that register all necessary toolchains. |
Requires register_toolchains() in MODULE.bazel ; native.register_toolchains() in an extension raises an error. Each module's MODULE.bazel file can register its own toolchains automatically. The root module need only call register_toolchains() for a dependency's toolchains to customize toolchain resolution. |
To fix this, first copy the register_toolchain()
statement (without native.
)
from the macro into your MODULE.bazel
file. If the macro consists solely of
native.register_toolchain()
, and no module extensions call it (directly or
indirectly), you're done. No patching necessary.
Otherwise, if the macro also contains other logic required by a module extension, then do one of the following:
- If the macro has a
register_toolchains
parameter or similar, call it from your own module extension withregister_toolchains = False
in the argument list. No patching necessary. - If the macro unconditionally calls
native.register_toolchains()
, either remove the code or add aregister_toolchains
parameter and call it withregister_toolchains = False
. Create a patch for the updated macro definition.
Avoiding native.bind()
calls¶
native.bind()
is completely disallowed under Bzlmod.
WORKSPACE |
MODULE.bazel |
---|---|
Allows bind() and native.bind() calls. |
bind() and native.bind() raise an error. Requires removing bind() targets and updating dependents to depend upon apparent repository name labels or alias() targets instead. |
If your module extension calls macros containing native.bind()
, here are your
options, all of which require patching the dependency:
- Remove the
native.bind()
call altogether. - Replace the original
native.bind()
call with analias
, possibly usingnative.alias()
, and using the samename
andactual
parameters. - If a macro has a
bind_targets
parameter or similar, call the macro from your module extension withbind_targets = False
. Depending on the originalbind()
target, you may need to create a newalias
target. - If you're afraid to remove the
native.bind()
call completely, add abind_targets = True
parameter or some such and call it withbind_targets = False
. You may still need to add analias
target.
In each of these cases, you may need to update any dependencies on the original
//external:
target with the actual
target or an alias
. If the
//external:
target appears in your dependencies, you will need to patch those
instances as well.
Adding a MODULE.bazel
file to a vendored repo to resolve conflicts¶
At one point, we had a vendored copy of the protocolbuffers/protobuf
library, a ubiquitous dependency in the Bazel ecosystem, that wasn't Bzlmod
compatible. At the same time, the bazel_tools
module baked into Bazel depends
on the protobuf
Bazel module. This caused our Bzlmod builds to fail due
to a combination of factors.
Since performing the below steps during our initial Bzlmod migration, we've
since upgraded to a properly Bzlmod compatible version of protobuf
. However,
if you're in a similar situation with another dependency, hopefully this
information provides helpful insights.
What follows isn't exactly a patch based solution, but it's in the ballpark, since applying a change to vendored repos is akin to patching.
Try override_repo from Bazel 7.4.0 when using module extensions.
override_repo()
(in Bazel 7.4.0 and later) enables the root
module (i.e., the main repository's MODULE.bazel
file) to override repos
defined by a module extension. In this case, however, there weren't module
extensions involved, and the problem ultimately called for overriding the
entire module.
The first part of the problem was that we set the following in our main repo's
.bazelrc
so that unused C++ functions break the build:
Setting -Werror=unused-function in our .bazelrc | |
---|---|
In our vendored protobuf
, we overrode this in its //build_defs:cpp_opts.bzl
:
Disabling unused function errors in vendored protobuf | |
---|---|
When beginning our Bzlmod migration, Bazel attempted to build two distinct
protobuf
versions configured for our project:
- A vendored copy of
protobuf
21.7 without aMODULE.bazel
file, instantiated in ourWORKSPACE
file. - The
protobuf
3.19.6 module required by@bazel_tools
, defined by theMODULE.bazel
file supplied by the Bazel Central Registry. This is the same version used by Bazel 7.4.1, 7.5.0, and several Bazel 8 prerelease builds. (src/MODULE.tools
inbazelbuild/bazel
becomes theMODULE.bazel
file in$(bazel info output_base)/external/bazel_tools
.)
Bazel attempted to build both protobuf
versions, since one was a WORKSPACE
repo and one was a module, so neither instance overrode the other. Both versions
contained the unused Offset()
function. The build then failed with an unused
function error when Bazel would try to build the protobuf
Bazel module
imported by bazel_tools
:
Our protobuf originally broke under Bzlmod. | |
---|---|
protobuf
21.7 was the highest Bazel module version available at the time. I
tried importing it with bazel_dep()
and patching it with
single_version_override()
to add -Wno-unused-function
to COPTS
in build_defs/cpp_opts.bzl
. This did not override the protobuf
module repo
from @bazel_tools
with our copy, so Bazel built both protobuf
modules,
causing a duplicate Java class error. (This may have been related to the problem
reported in bazelbuild/bazel: duplicate repos between WORKSPACE and MODULE when
using repo_name mapping #21818.)
The solution that worked at the time was adding our own custom MODULE.bazel
file to our vendored protobuf
copy and using local_path_override()
:
Using our protobuf clone in MODULE.bazel | |
---|---|
With our vendored copy acting as a proper Bazel module, Bzlmod ensured it was
used throughout the dependency graph, including in the bazel_tools
module. I
won't go into the details of that MODULE.bazel
file, since newer protobuf
versions now contain their own.
Sharing vendored repositories with a vendored module¶
This solution also required adding a module extension to our project to share
other vendored repositories between protobuf
and the root module. It
instantiates the other vendored repos using local_repository()
.
The root module and protobuf
then bring these repos into scope via
use_extension()
and use_repo()
.
override_repo()
and inject_repo()
¶
override_repo()
or inject_repo()
enable the root module to choose its
own repositories for use by a dependency's module extension:
override_repo()
is appropriate for overriding a repository of the same name as one already used by a dependency, and doesn't require patching.inject_repo()
is appropriate when introducing a new repo while patching a dependency.
Prefer one of these to writing a custom module extension when a vendored repo already has a module extension for instantiating repos or applying patches.
Hacking around Windows path length issues due to longer canonical repo names¶
After enabling Bzlmod, our Windows continuous integration build failed due to a problem when building the rules_rust Bazel module:
It turns out that path is 239 characters long. It comes under the apparent 260 character limit, but appears to grow past the limit after it's converted to an absolute path internally.
There at least two existing issues reflecting this problem:
- rules_rust is broken with Bazel@HEAD for windows platform #1946
- Rename crates_vendor_manifests to cvm to shorten windows path lengths #1944
Apparently another fix exists, if we knew where to patch it in:
The maintainers originally made the decision to disable the rules_rust Windows
builds instead, due to a lack of a dedicated Windows maintainer.
This remained true as of rules_rust
version 0.54.1, though
bazelbuild/rules_rust#3023 restored the builds for rules_rust
0.55.0.
(There's still an open issue to renable Windows crate_universe
example
tests at some point.)
At the time, we used rules_rust
0.41.1, and I realized that these platform
specific path segments contained largely redundant information. I created the
following patch to remove the middle component, known as the target triple (or
triplet). (I've stripped out the comments I'd included in the original,
which I used almost verbatim to write this section of the post.)
The savings were substantial, and fixed our Windows builds:
Configuration | Path Component | Length |
---|---|---|
Before, under bzlmod | rules_rust~~rust~rust_windows_x86_64__x86_64-pc-windows-msvc__stable_tools |
74 |
Before, under WORKSPACE | rust_windows_x86_64__x86_64-pc-windows-msvc__stable_tools |
57 |
After, under bzlmod | rules_rust~~rust~rust_windows_x86_64__stable_tools |
50 |
The "proper" fix may still be to find places to inject the UNC path prefix, or coaching users to enable long paths on Windows somehow. But in the meanwhile, this got our own builds working again quite quickly and reliably.
A confession...
I've been meaning to try contributing this change upstream, to see if the maintainers would accept it. I may yet do that, though bazelbuild/rules_rust#3023 seems to work for now.
protobuf is dropping Windows + MSVC support
On a related note, protobuf is dropping support for Windows Microsoft Visual C++ builds because of the path length issue. As mentioned in the Conclusion, I'll write more about this eventually.
Conclusion¶
Writing my own MODULE.bazel
files, module extensions, and patches for external
dependencies helped me complete EngFlow's Bzlmod migration. I'm hopeful that
sharing these insights and techniques will help others make progress on their
own migrations without waiting for all their dependencies to migrate.
I've mostly prioritized getting our Bzlmod migration done and publishing about
the process first, but I'm also now trying to contribute changes upstream. I've
almost finished contributing to the Bzlmodification of rules_scala
,
including the changes described in these blog posts and then some. Contributing
upstream has taken a lot more time, but helps a lot more people, which I
find rewarding in ways that transcend working only internally. Even so, these
contributions wouldn't've been possible without having gone through this
process, as I learned a lot and had working production code in hand.
For the next post, I'll cover more examples of repo name dependencies and how to resolve them. Technically, these are also patchable problems, but there's enough of them that they warrant yet another dedicated post.
I'm no longer sure I'll write the rules_nodejs
post mentioned in previous
posts. While I dove deeply into Bzlmodifying rules_scala
, my colleague Ricard
Solé migrated our code from my heavily patched 5.8.5 to
aspect_rules_js. It's not officially canceled, but its fate is uncertain.
However, here are additional Bzlmod related topics that I'll very likely cover:
-
"Toolchainization", or packaging toolchain dependencies in a rule set behind a convenient API, compatible with both
WORKSPACE
and Bzlmod. I'll use the New scala_toolchains() API for WORKSPACE and upcoming Bzlmod API for rules_scala to illustrate this. -
Protocol compiler toolchainization, which I implemented in bazelbuild/rules_scala#1710 to fix a breakage when recompiling
protoc
on Windows fromprotobuf
v25.5 onwards. This breakage was also due to the 260-character path length limit, and is the reason why protobuf is dropping Windows + MSVC support. This left protocol compiler toolchainization as the only way to fix Windows builds after updatingprotobuf
.
As always, I'm open to questions, suggestions, corrections, and updates relating to this series of Bzlmodification posts. It's easiest to find me lurking in the #bzlmod channel of the Bazel Slack workspace. I'd love to hear how your own Bzlmod migration is going—especially if these blog posts have helped!