Skip to content

Migrating to Bazel Modules (a.k.a. Bzlmod) - The Easy Parts

You may be aware that Bazel will remove support for WORKSPACE in Bazel 9 in favor of Bazel Modules (a.k.a. Bzlmod). The current mainstream release is Bazel 7.2.0, so there's plenty of time to migrate. However, there's no time like the present to get started, to avoid further WORKSPACE dependencies and a pile of migration work in the future.

I recently completed the Bzlmod migration for EngFlow/example and our internal repos. This experience taught me a lot about Bzlmod and about migrating complex projects with challenging dependency issues that I'll share over a few blog posts. I'll also borrow from Sara Adams's earlier post, in which she described an example bzlmod migration based on EngFlow's Bazel Invocation Analyzer repo.

This article is part of the series "Migrating to Bazel Modules (a.k.a. Bzlmod)":

Background

If you're only starting to learn about Bzlmod, please review the latest version of Bazel's External dependencies documentation first, especially the Bzlmod Migration Guide. This post provides ample links throughout for additional context and details, but it will read more smoothly if you read the official Bazel docs first.

The Bazel documents cover Bzlmod concepts in more detail than this blog post series will, as well as advanced use cases we fortunately didn't require. So if this blog series doesn't cover all your needs, you may find relevant information there.

That said, I'll share useful insights and techniques that I didn't find apparent in the Bazel documentation or that of its rule sets.

The Good

Migrating your project is probably easier than you expect. Most dependencies and language rule sets already support Bzlmod. In this section, we'll see how to deal with the common cases. Later, we'll see how to address some more difficult issues you might see along the way.

Standalone applications can migrate gradually without WORKSPACE.bzlmod

The good news for standalone application projects is that you don't have to migrate everything and swap out WORKSPACE for MODULE.bazel all at once. When Bzlmod is enabled (via --enable_bzlmod or by default in Bazel 7), it will parse both MODULE.bazel and WORKSPACE for external dependencies. There's also no need for an intermediate WORKSPACE.bzlmod file during the migration process.

This means you can gradually move individual dependencies directly from WORKSPACE to MODULE.bazel, keeping the build intact at every step without duplicating dependency information. This makes the migration process far more palatable and manageable for large, complex codebases.

You can begin making headway on the Bzlmod migration at a relaxed pace, saving more challenging dependencies for later. You can revisit them when you have more bandwidth, more experience, the problems have solved themselves upstream, or when you decide to drop the dependency.

Difference from the Bzlmod Migration Guide's recommended process

The advice in the Bzlmod Migration Guide suggests creating a WORKSPACE.bzlmod file to switch between WORKSPACE and Bzlmod modes during the migration. It also recommends a migration process to build up MODULE.bazel and WORKSPACE.bzlmod independently from WORKSPACE.

This advice appears geared towards projects with relatively straightforward dependencies, or projects (such as Bazel rule sets) that themselves are dependencies of other Bazel projects. It also leads to duplication of dependency information between WORKSPACE and MODULE.bazel + WORKSPACE.bzlmod.

Such duplication may be necessary for some projects that are themselves Bazel dependencies of other projects, in order to support non-Bzlmod builds. However, it is not necessary for projects which have no requirement to support non-Bzlmod builds.

Lifting and shifting most dependencies

You can "lift and shift" the vast majority of dependencies straight from WORKSPACE to MODULE.bazel rather easily. This section describes some of the most common mechanisms for doing that.

With bazel_dep()

In many cases, you can use the Bazel Central Registry (BCR) to find the equivalent bazel_dep() to replace existing http_archive() repository rules, if available.

You'll still need to refer to a module repository's README.md, release notes, or other documentation if additional configuration is necessary. However, this configuration is often greatly simplified compared to the equivalent WORKSPACE configuration. (See the rules_jvm_external example below.)

The BCR contains all the information necessary to locate repository archives and verify their integrity. The "Fetch external dependencies as Bazel modules" section of the Bzlmod Migration Guide illustrates how to do exactly this.

For a concrete example, see Sara's pull request EngFlow/bazel_invocation_analyzer: Start using Bzlmod #144. She was able to replace thirty-eight lines of WORKSPACE configuration with the following three lines in MODULE.bazel:

Text Only
1
2
3
bazel_dep(name = "bazel_skylib", version = "1.2.1")
bazel_dep(name = "platforms", version = "0.0.5")
bazel_dep(name = "rules_proto", version = "4.0.0")

Example

See Sara's Move buildifier setup from WORKSPACE to Bzlmod #153 for an even more dramatic simplification afforded by bazel_dep(). This change involved replacing several http_file() downloads of Buildifier binaries and a complicated copy_file() rule with a bazel_dep() and a brief buildifier_binary().

Defining your own custom Bazel module registry

If you'd prefer not to depend on the BCR or other external systems beyond your control, you can define your own custom Bazel registry. This allows you to depend on your own custom archive mirrors for your bazel_dep() targets. (See Jay Conrod's post "How GitHub's upgrade broke Bazel builds" for reasons why you might want to do this.) Or you can vendor your dependencies in your codebase—but see the local_repository() and local_path_override() caveats below.

With http_archive() or any other repository rule

Even if no Bazel module is available in the registry, http_archive() and its kin are still available as repository rules in Bzlmod. In fact, you can still use any repository rule from WORKSPACE directly in MODULE.bazel by replacing load() with use_repo_rule(). Then if the repository doesn't require any further setup, you're done!

The "Fetch external dependencies with module extensions" section of the Bzlmod Migration Guide has a straightforward example of using use_repo_rule() to import http_file():

Text Only
1
2
3
4
5
6
7
## MODULE.bazel
http_file = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_file")
http_file(
    name = "data_file",
    url = "http://example.com/file",
    sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
)

With rules_jvm_external

Java projects using Bazel often use rules_jvm_external to manage Maven-style dependencies on external JARs. Under WORKSPACE, this involves a series of load() statements and dependency setup calls.

Alternatively, Java projects may rely on a series of http_jar() calls in the WORKSPACE file. This was the case before Sara's EngFlow/bazel_invocation_analyzer: Use Bzlmod instead of http_jar in WORKSPACE file #149. She replaced fifteen http_jar() calls by using the maven extension from rules_jvm_external.

She then followed this with Pin Maven artifacts with maven_install.json #160 and Remove transitive Maven deps #161. After the updates from Update to rules_jvm_external 6.2, repin maven deps #183, this looks like:

Text Only
bazel_dep(name = "rules_jvm_external", version = "6.2")

maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven")
maven.install(
    artifacts = [
        "com.google.code.findbugs:jsr305:3.0.2",
        "com.google.code.gson:gson:2.11.0",
        "com.google.guava:failureaccess:1.0.2",
        "com.google.guava:guava:33.2.1-jre",
        "commons-cli:commons-cli:1.8.0",

        # For Tests
        "com.google.googlejavaformat:google-java-format:1.22.0",
        "com.google.truth:truth:1.4.3",
        "com.google.truth.extensions:truth-java8-extension:1.4.3",
        "junit:junit:4.13.2",
        "org.mockito:mockito-core:5.12.0",
    ],
    # When updating versions, run `REPIN=1 bazel run @maven//:pin`
    fail_if_repin_required = True,
    lock_file = "//:maven_install.json",
    repositories = [
        "https://repo1.maven.org/maven2",
    ],
)
use_repo(maven, "maven")

After the migration, it's also now easier to update versions, and there's no need to explicitly download transitive dependencies. Bazel can also avoid unnecessary downloads while still applying integrity checks.

Tip

See the rules_jvm_external Bzlmod documentation for further details on Bzlmod-specific configuration. See the "Pinning artifacts and integration with Bazel's downloader" section of the rules_jvm_external README for the benefits of artifact pinning in particular.

With other language-specific rule sets

Rule sets exist for several other languages that integrate external dependency managers into the Bazel external repository scheme, including (but not limited to):

Module version resolution

WORKSPACE only allows one version of a repository to exist in the build. You're allowed to declare multiple versions, but Bazel has very subtle rules on how it picks versions. This can be dangerous if you end up linking in multiple incompatible versions of a library with significant runtime presence, like gRPC.

Tip

See Jay Conrod's post on "Organizing Bazel WORKSPACE Files" for a deep dive into WORKSPACE evaluation and repository selection.

Bzlmod, in contrast, resolves multiple versions of the same module throughout the dependency graph. Its algorithm replaces all compatible minor versions with a single version, while allowing multiple incompatible major versions to exist in the build.

Module version resolution override mechanisms exist to provide control over specific dependency versions. For example, multiple_version_override() enables multiple versions to exist in the build that the module resolution algorithm would otherwise elide.

Updated repository path encoding

There are two key differences in how Bzlmod encodes repository paths that generally improve how Bazel works. However, it may affect your project if it depends on such paths directly (as discussed in later sections).

Stable _main module name

Under bzlmod, the runfiles root for the main repo is always _main/. From the ctx.workspace_name() definition:

The name of the workspace, which is effectively the execution root name and runfiles prefix for the main repo. If --enable_bzlmod is on, this is the fixed string _main. Otherwise, this is the workspace name as defined in the WORKSPACE file.

This means that any runfile paths to other files in your codebase will always begin with _main. More on this below.

Canonical repository names

To support module resolution, Bazel has the concept of canonical repository names (starting with @@) as distinct from apparent repository names (starting with @):

  • @ repository names are used within each repository to refer to other repositories it directly depends on. They may not be globally unique.

  • @@ names are used globally by Bazel and the command line user to refer to a specific instance of a downloaded repository. They are "mangled" to encode the module name and the module extension that imported it, as well as version information if necessary.

Info

By default, canonical repo names do not encode the module version since Bazel 7.1.0. They include version numbers only if the build requires multiple versions of the same module.

From the Module extensions: Repository names and visibility section of the Bazel documentation:

Repos generated by extensions have canonical names in the form of module_repo_canonical_name~extension_name~repo_name. For extensions hosted in the root module, the module_repo_canonical_name part is replaced with the string _main.

This means that the repository directory names in $(bazel info output_base)/external encode useful information showing how a repository is related to the build. This allows multiple repository versions to exist if necessary, avoiding one silently overwriting the other. It can also be useful when trying to investigate problems with the build.

Examples of canonical repository names

  • bazel_skylib~: The @bazel_skylib repository defined by a bazel_dep() rule in one or more MODULE.bazel files in the dependency graph. Since module resolution resulted in a single version, there is no version information in the repo name.
  • _main~_repo_rules~com_github_grpc_grpc: This is the @com_github_grpc_grpc repository defined by an http_archive() rule (i.e., a _repo_rule) in the main repository's MODULE.bazel (specified by _main). There is no version information encoded, since http_archive() doesn't contain a version attribute.
  • rules_jvm_external~~maven~junit_junit_4_13_2: The JAR repository backing the @maven//:junit_junit target defined by the maven module extension from the rules_jvm_external module.
  • rules_jvm_external~5.3~maven~junit_junit_4_13_2: Same as the previous name, except multiple modules in the dependency graph depend on different incompatible versions of rules_jvm_external. (That, or the project was built using a version of Bazel prior to 7.1.0.)

Info

See also Benjamin's post The Many Caches of Bazel for information on the repository cache.

The Bad

Of course, not everything is going to work out of the box. Here are some more key differences of the Bzlmod model from WORKSPACE, along with a few related breakages.

In later posts, outlined in The Hard Parts, I'll dive into more details of why these differences exist and how to remedy complicated migration challenges.

Enabling Bzlmod may require many fixes at once

The act of setting --enable_bzlmod=true may break many things before moving a single repo from WORKSPACE to MODULE.bazel. In particular:

Each of these issues can and should be fixed one at a time, in separate commits, while migrating as few repositories to MODULE.bazel as possible. Still, the first overall change to enable Bzlmod may require fixing many of these problems at once to keep the overall build intact.

Not all repositories are (totally) Bzlmod-ready

While seemingly most major Bazel rules packages have migrated to Bzlmod, there are notable exceptions, such as rules_scala. Or perhaps you depend on a deprecated rule set like rules_nodejs. Upgrading to a newer, Bzlmod-ready rule set is absolutely recommended, but may require substantially more effort, given fundamental differences between the two rule sets.

In these cases, you can still move forward with migrating to Bzlmod, but it requires a bit of insight and custom work. Many of the issues are related to having to load() configuration functions that implicitly define toolchains and other repositories as described below.

Some repositories behave differently with Bzlmod enabled

Some rule sets contain logic to behave differently under Bzlmod, and/or expose an API lacking features from the earlier WORKSPACE API. As such, you may not have a choice but to migrate such repositories to MODULE.bazel immediately.

Usually this should prove easy, as such rule sets should have a well documented migration path to all cover common cases. However, if you're applying patches or any other sort of custom configuration, and the documentation and examples are lacking, resolving issues could require careful study.

rules_python

For example, enabling Bzlmod causes rules_python to explicitly disable toolchain registration inside python_register_toolchains()preventing its WORKSPACE API from working at all. This is because module extensions can't call native.register_toolchains(), but this disables Python toolchain registration even when calling python_register_toolchains() directly from WORKSPACE.

Info

Tracing through the code, you can see that the rules_python Bzlmod extension also calls python_register_toolchains(). This function takes a register_toolchains parameter, so the module extension could pass register_toolchains=False instead of having python_register_toolchains() detect Bzlmod enablement. It's possible to change this upstream, and there is an open issue for it: Enabling bzlmod causes workspace toolchains to no longer be registered #1675. But it's probably better to migrate your rules_python usage to Bzlmod regardless, rather than contributing or waiting for an upstream fix.

In other words, when you enable Bzlmod, you can no longer use rules_python in WORKSPACE. You must move it to MODULE.bazel. This, in turn, forces you to contend with API changes such as:

  • Under WORKSPACE: The python_register_toolchains macro accepts a tool_versions parameter to provide fine-grained control over archive sources for python versions. Its python_version parameter can be specified down to the patch level, e.g., 3.12.4.
  • Under Bzlmod: The python extension exposes the python.toolchain tag, which currently does not provide a tool_versions parameter. In versions earlier than 0.32.0, which resolves rules_python #1371, its python_version parameter only accepts values specified down to the minor version, e.g., 3.12. The extension uses this python_version to select an archive from its own internal table.

Similarly, when it comes to applying package modifications:

  • Under WORKSPACE: pip_parse accepts an annotations parameter to apply updates to imported packages, as a dictionary mapping package names to package_annotation objects.
  • Under Bzlmod: pip.parse accepts a whl_modifications parameter with a different interface: a dictionary of repo target labels, defined using the pip.whl_mods tag, to package names.

Tip

The best example of pip.whl_mods usage that I've found is in examples/bzlmod/MODULE.bazel from bazelbuild/rules_python.

New naming conventions break file path dependencies

While a stable _main module name and a new canonical repository name schema provide long term benefits, both may introduce immediate pain. The Bazel documentation has this to say about the canonical repository name format in Bazel modules: Repository names and strict deps (emphasis theirs):

Note that the canonical name format is not an API you should depend on and is subject to change at any time. Instead of hard-coding the canonical name, use a supported way to get it directly from Bazel...

Targets that don't handle Bazel repository and/or runfiles paths in a portable way can cause problems:

  • Rules or programs that depend directly on paths in the execroot, rather than getting them via runfiles libraries or predefined source/output path variables, may break. Paths to specific files or directories within a filegroup target are especially susceptible to this kind of breakage.
  • Rules that repackage external archives using rules_pkg and that strip the repository path prefix may build successfully, but encode broken file paths. This may be caught by verify_archive_test() from rules_pkg, but will otherwise pass undetected.

Hardcoding these paths to contain the current canonical repo names may get things working, but are vulnerable to breaking again at any time. It is possible to update such paths to properly include the new canonical repository names in a portable way. See the The Not Quite Ugly, Yet Not That Great, But It'll Do section below.

Runfiles libraries

Runfiles libraries are available for different languages to enable tests or other programs to locate their runfiles programatically during execution. I'll discuss runfiles libraries in greater detail in the next post in this series.

Some tools don't yet grok MODULE.bazel

Some tools may not yet handle MODULE.bazel (ibazel), or you may need to upgrade (bazelisk). Even Bazel's own local_repository() and local_path_override() rules don't yet grok repos in the same source tree.

A lot of this has to do with a tool seeing WORKSPACE as a repo boundary, but not yet seeing MODULE.bazel (or REPO.bazel) as such.

load() is not allowed in MODULE.bazel

WORKSPACE files are evaluated top-to-bottom, and allow load() statements anywhere, in order to load macros from .bzl files within an http_archive() repository. These macros register the repository's own dependencies, as well as the toolchains and other repositories that it provides.

Using load() in a MODULE.bazel file, however, will produce an error:

Text Only
ERROR: MODULE.bazel:10:1:
       `load` statements may not be used in MODULE.bazel files

Because MODULE.bazel files are evaluated differently than WORKSPACE files , load() statements aren't allowed. So if a Bazel module isn't yet available for a dependency...

Module extensions are often necessary

...you may need to write your own module extension (or extensions) to configure it. Module extensions aren't too different from other functions defined in.bzl files, but they are required to perform configuration tasks previously performed directly in WORKSPACE. Any http_archive() followed by load() and a macro call in WORKSPACE will require adding a module extension.

You can call http_archive() in MODULE.bazel to define a repository, then define an extension in a .bzl file to load() and invoke its configuration macros. Then you'll call the use_extension() function in MODULE.bazel (instead of load()) to bring the extension into scope. After resolving the module dependency graph, Bzlmod will then evaluate the extension, invoking its implementation function to instantiate its repositories.

Warning

You can't call http_archive() and load() a macro file from it in the same module extension. All load() statements must appear at the top of the extension's .bzl file, before any function definitions that may call http_archive(). In this way, Bzlmod forbids dependencies between repositories instantiated within the same extension, which macros have the potential to introduce.

Implicit repository registration is not supported

One of the biggest problems stemming from the inability to load() configuration macros from a repository in MODULE.bazel is registering additional repositories.

Under WORKSPACE, there is no built-in mechanism for resolving transitive repository dependencies. The idiom of load()ing a file from a repo to invoke a macro to configure its dependencies arose in response to this. There was also no need to reference these transitive dependencies directly in the WORKSPACE file.

Under Bzlmod, each Bazel module will define the repos it requires without requiring you to reference it explicitly. At the same time, all repos referenced by your project's BUILD files must be brought into scope within MODULE.bazel itself. You must do so using bazel_dep(), repository rules imported from module extensions by use_repo_rule() (such as http_archive()), or by calling use_repo() on a module extension. The benefit is that you can see how every direct dependency repository is configured all within MODULE.bazel.

Importing repositories that aren't Bzlmod-ready into MODULE.bazel requires dealing with the following consequences of these design differences from WORKSPACE:

  • You have to write your own module extension separate from MODULE.bazel to load() and invoke these configuration macros.
  • You have to import each transitive dependency repo generated by these macros directly into MODULE.bazel. This is because there is no Bazel module other than _main that can claim ownership of these transitive dependencies.

Fortunately, while sometimes tedious, writing these module extensions and bringing them into scope with use_repo() isn't that hard. It can get tricky when a repo defines additional layers of repos that depend on one another.

Multiple repo versions may manifest, causing breakage

During the migration, you may introduce multiple versions of a repository without realizing it at first. If you depend directly on a non-Bzlmod version of a repository, a Bazel module dependency may introduce a second, Bzlmod-ready version as a transitive dependency.

This may not break the build at all—but it might. If it does, you may have to add a MODULE.bazel to the non-Bzlmod version and write a module extension to resolve the incompatibility.

native.register_toolchains() and native.bind() are not supported

The other big problem stemmming from the inability to load() configuration macros deals with registering toolchains.

Module extensions do not support native.register_toolchains() or native.bind() calls. For fully Bzlmod-compatible dependencies, that module's extensions should provide a suitable API for configuring toolchains. For non-module dependencies, in some cases, configuration macros tailored for WORKSPACE that lead to native.register_toolchains() or native.bind() calls won't work. Whatever toolchains such macros used to register must be specified explicitly in MODULE.bazel or wrapped in a custom module extension.

MODULE.bazel.lock can be noisy

The MODULE.bazel.lock file enables Bazel to resolve dependencies more efficiently and deterministically than WORKSPACE. By default, it will also show when dependency information changes based on updates to MODULE.bazel and any module extensions.

Unfortunately, at the moment, it can also be a bit noisy. Adding and updating dependencies, particularly in large batches during a migration, generates huge diffs. Having different people use different versions of Bazel and related tooling can also result in diffs due to thrashing between lock file versions. The Module Extensions section is a particular source of volatility as well. The documentation even suggests using an automated git merge driver to resolve lockfile conflicts.

If you and your team are comfortable with the noise, or with configuring the merge driver, then go ahead check the lockfile into source control. Otherwise, you may wish to .gitignore the lockfile until after the migration, and after the tools and your team have gotten comfortable with Bzlmod.

Tip

Doing so won't break the build, as Bazel will update the file locally as necessary; the changes just won't appear in git diff. This effectively maintains parity with the inability of WORKSPACE to document resolved dependencies. However, you may miss some of the benefits of detecting and validating dependency updates, so do so judiciously.

The Not Quite Ugly, Yet Not That Great, But It'll Do

Despite some of these rough edges, there are some easy fixes and workarounds for some of the most common problems.

Fix broken runfiles paths using runfiles libraries or Bazel variables

Use runfiles libraries in tests and other programs to map runfile paths to their actual locations under Bzlmod. Alternatively, apply predefined source/output path variables in your rules to inject runfile paths into target programs via command line arguments or env properties. There are several variations of doing so that we'll cover later, if the way forward isn't immediately obvious.

Fix broken paths with _main (temporarily)

As a special case, for broken runfiles paths that contain the previous workspace() name, replace that name with _main.

Longer term, translating such paths using runfiles libraries or injecting them via Bazel variables is the proper solution. But if updating all broken targets will take a long time for any reason, this temporary approach won't make existing uses any worse. It also provides an easily greppable string to help discover files when you are ready to try applying runfiles libraries or Bazel variables instead.

@@ is the new @

In Bazel 7.0.0 and earlier, the repository label @ (as in @//foo) referred to the main repository. In Bazel 7.1.0 and later, the repository label @@ now refers to the main repository (as in @@//foo):

Labels starting with @@// are references to the main repository, which will still work even from external repositories.

The fix is to replace all instances of @// in your BUILD files with @@//.

Conversely, if your project contains references to external repositories starting with @@ (possibly a typo), you need to update them to start with @ instead. For example, @@repo would become @repo.

Info

I'm pretty sure this works when using WORKSPACE, even since Bazel 7.1.0, for two reasons. First, because WORKSPACE doesn't mangle canonical repository names. Second, because Bazel's Label::getShorthandDisplayForm() method elides @@repo to @repo in the context of the main repository.

Keep WORKSPACE around for tools don't yet grok MODULE.bazel

Keep an empty WORKSPACE file around until you can upgrade the tools in question. Better yet, have WORKSPACE contain only comments explaining why it's there and when it can go away. For example:

Text Only
# Exists only to satisfy ibazel until the following PR is merged and released:
# - https://github.com/bazelbuild/bazel-watcher/pull/647

Ignore local_repository() and local_path_override() paths

Until the Bazel team resolves bazelbuild/bazel#22208, you can add any local_repository() and local_path_override() to your repository's .bazelignore file. This will enable you to build the project successfully—but it may interfere with your IDE's autocompletion feature.

Potential implications of (the not yet official) Vendor Mode

Bazel now advertises a Vendor Mode feature that may prove useful for copying external dependencies into your main repository, appearing in version 7.3.0rc1. It appears to be orthogonal to local_repository() and local_path_override(). (i.e., You can use Vendor Mode to copy the dependencies, but you may still need the local_* rules to build them.) See also: bazelbuild/bazel: Bzlmod: vendor mode #19563.

The Hard Parts

Here's a list of deeper Bzlmod migration topics I plan to cover in depth. A number of these (but not all of them) involve working around problems in your dependencies.

Arguably one should wait for problems to be fixed upstream, or you should contribute upstream fixes yourself. You can still migrate as many dependencies as you can to Bzlmod, and keep your WORKSPACE in place for the rest. However, if you want to get on with completing your own Bzlmod migration, you need not necessarily remain blocked by your dependencies.

You may find some of the upcoming advice useful—and applying it might even help you develop fixes to contribute upstream. Either way, you should be able to safely discard any dependency workarounds once they're no longer needed.

This isn't the hardest class of problem to solve, but it requires careful handling to avoid depending on the canonical name format itself.

  • Translating file paths via runfiles libraries
  • Injecting file paths via predefined source/output path variables (runfiles, etc.)
  • Injecting canonical repo names programmatically via genrule()s
  • Injecting canonical repo names as custom Make variables
  • Tactics for updating rules_pkg targets that need to strip file prefixes from external repository paths

Solving problems with module extensions

These posts will cover additional Bzlmod details accounting for specific challenges and provide fitting solutions (outside of the dependencies themselves becoming Bzlmod-ready).

  • Defining repositories using load()ed constants
  • Avoiding circular repository definitions between MODULE.bazel and extensions when migrating a series of related WORKSPACE statements

Patching external repos/modules to solve tricky problems

When in doubt, use brute force—i.e., patch your dependencies.

These are some of the problems you can solve using patches in http_archive(), archive_override(), and other repository rules to work around problems in your dependencies. (Until the problems are properly fixed upstream, of course.)

  • Avoiding native.register_toolchain() and native.bind() calls
  • Resolving breakages from non-Bzlmod and Bzlmod-ready versions of a repo in the same build
  • Hacking around Windows path length issues due to longer canonical repo names
  • De-mangling canonical repo names to fix breakages in a low-risk way

Migrating from rules_nodejs to rules_js + rules_ts

This will be a series in itself, given the fundamental differences between rules_nodejs (deprecated and not Bzlmod-ready) and rules_js (Bzlmod-ready):

  • rules_nodejs exposes an @npm repo that makes all transitive dependencies accessible.
  • rules_js contains a new js_library implementation with different providers that are required by its other rules (i.e., the js_library from rules_nodejs won't work with them). It also exposes a more restrictive set of //:node_modules targets that are less tolerant of improperly declared transitive dependencies.

In addition to dependency graph details, there's also the matter of how different npms adapt to these different repository models and their implementation details.

Conclusion

Bzlmod may not be perfect, but it does provide many benefits over the previous WORKSPACE model. However, for large, complex code bases deeply dependent on the existing WORKSPACE ecosystem, migrating to Bzlmod may require some persistence, insight, and ingenuity.

This post, and the future posts in this series, aim to make some key Bzlmod migration insights and techniques more accessible. Even if your codebase presents specific challenges not explicitly covered by these posts, hopefully they at least provide sufficient inspiration to help you overcome them.

Stay tuned—there's lots more fun to come!


Updates

2024-08-09

2024-07-31

  • Updated the rules_python section to clarify that version 0.32.0 added support for Python versions down to the patch level. (Hat tip to Ignas Anikevicius for the notification.)
  • Updated to mention runfiles libraries throughout, and to mark the _main path component fix as temporary.
  • Updated Vendor Mode info to note its inclusion in Bazel 7.3.0rc1.

2024-07-12

  • Updated the rules_jvm_external example to use version 6.2, which eliminates the need for the unpinned_maven repo. (Hat tip to Simon Stewart for the notification.)
  • Added a reference to the rules_python issue corresponding to the described WORKSPACE breakage when enabling Bzlmod.
  • Added a reference to the upcoming change to the canonical repo name format to address Windows performance issues.
  • Clarified that adding MODULE.bazel.lock to .gitignore effectively matches the inability of WORKSPACE to document resolved dependencies.
  • Updated link to Bazel 7.3.0 Vendor mode issue to reflect its current "not planned" status.