Migrating to Bazel Modules (a.k.a. Bzlmod) - Toolchainization¶
Part of the promise of Bazel modules is that they are largely self-initializing in an order-independent way. Rule sets, in particular, no longer need to burden users with importing and invoking macros to instantiate repositories and toolchains in a specific order. This burden now shifts to rule set maintainers, but the existing implementation may not provide this ease of use without modification.
This post describes the introduction of a new "toolchainized" API for
rules_scala v7.0.0 that better encapsulates toolchain configurations and
dependencies. We'll see how this new design enables optimal Bzlmod
compatibility, while simultaneously shrinking the legacy WORKSPACE
API surface
without losing functionality. We'll also see how the Bzlmod and legacy
WORKSPACE
APIs provide similar interfaces while sharing the same underlying
implementation, facilitating Bzlmod migrations.
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
- Migrating to Bazel Modules (a.k.a. Bzlmod) - Repo Names, Again…
- Migrating to Bazel Modules (a.k.a. Bzlmod) - Toolchainization
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:
You may also choose to read Bazel's Toolchains documentation first, or read through this post and come back to that document later.
What are toolchains?¶
Bazel's Toolchains mechanism decouples and hides sets of build tools and
their dependencies from the rules and targets that rely upon them. Each concrete
set of build tools and their dependencies is called a toolchain
.
The toolchain_type concept provides the interface that links concrete build
tool implementations to rules at build time. This is reminiscent of dependency
injection, whereby the toolchain_type
creates a seam that enables
switching between different toolchains as necessary.
Toolchain selection is based on toolchain_type
and other constraints.
Whereas the toolchain_type
defines the interface, Bazel relies on
constraints specified by rule and toolchain targets to select a specific
implementation. Two of the most common constraints are operating system and
CPU architecture, which are often bundled into platform
configurations.
As we'll see, rules_scala
applies the Scala language version as a
toolchain constraint. For more details, read the Bazel documentation on
Platforms, Configurable Build Constraints, and Platforms and
Toolchains Rules (after reading this blog post, of course).
Rule sets already must ensure their rules apply the correct toolchains for a
particular target by declaring dependencies on toolchain_type
s. Properly
designed rule sets also encapsulate toolchain dependencies to prevent users
needing to reference specific toolchain targets or their dependencies
directly.
To achive this design, I find it useful to think of toolchains as comprising a separate dependency graph from the rest of the build. Again, this is analogous to using dependency injection to hide concrete dependencies from client code. This post describes how to achieve this encapsulation of the toolchain dependency graph.
Of course, good design can provide both convenience and flexibility. A properly designed rule set will give the user option to exert control over toolchain definitions and dependencies as well.
User defined toolchains are a topic for a future post.
This post describes how to provide a convenient default toolchain interface
that enables some degree of user configuration. rules_scala
has provided
macros for defining completely custom toolchains for a while, but no module
extension currently supplies the same functionality. I've begun to
experiment with defining such an extension (or extensions), which I may
cover in a future post.
Comparing differences between the Bzlmod and legacy WORKSPACE
models¶
An earlier post in this series contains a table contrasting the legacy
WORKSPACE and Bzlmod repository instantiation models. Please keep that table handy for a
more detailed, side-by-side comparison of legacy WORKSPACE
vs. Bzlmod
differences while reading this post. Many of those differences pertain to the
toolchain setup and registration issues described below.
The inconvenience of importing toolchain dependencies¶
The Module Extensions post described how to create
your own module extension for a dependency that hasn't yet migrated to
Bzlmod. Though feasible, it's annoying as hell, since you have to import all the
repositories your dependency needs into your own MODULE.bazel
file. (Not to
mention having to patch any Bzlmod related breakages.)
For example, this was the EngFlow/example configuration for rules_scala v6.6.0:
This configuration was a consequence of Bzlmod and legacy WORKSPACE
characteristics for which the user supplied module extension had to compensate:
-
Legacy
WORKSPACE
files can call macros that instantiate repositories, including those for toolchain dependencies, into the single global scope shared by all repositories. These macros can also call native.register_toolchains. This is why legacyWORKSPACE
files don't explicitly reference most repositories directly. -
MODULE.bazel
can't call macros. Module extensions can call macros, which in turn invoke repository rules, but each extension also has its own scope, separate from the invoking module's scope. Hence, modules often must define extensions to instantiate their toolchain dependency repositories, which are visible only within the extension's scope by default. -
MODULE.bazel
must callregister_toolchains
; module extensions can't. This means toolchain targets passed intoregister_toolchains
calls must be visible within the module's scope. -
Also worth noting: Under the legacy
WORKSPACE
model, only the main repository can invokeregister_toolchains
. Under Bzlmod, any module can invokeregister_toolchains
, not just the root module. We'll see how this comes into play shortly.
The consequences of these constraints are:
-
Toolchain targets defined in
BUILD
files of repositories imported by the module are visible within the module's scope. For example,@io_bazel_rules_scala//scala:toolchain
is visible within theEngFlow/example
module, becauseMODULE.bazel
imports@io_bazel_rules_scala
. -
Toolchain dependencies instantiated by a module extension are not visible within the invoking module's scope by default. For example, the
scala_deps
extension from//scala/extensions:deps.bzl
callsscala_repositories
from@io_bazel_rules_scala//scala:scala.bzl
. However, the instantiated repos are not visible to@io_bazel_rules_scala//scala:toolchain
. They're only visible to other repos instantiated within the same module extension. (Hint: This fact plays a role in our later design...) -
Therefore, the
EngFlow/example
module must bring each toolchain dependency repository from thescala_deps
module extension into its scope viause_repo
. Only then are they visible to@io_bazel_rules_scala//scala:toolchain
.
Bazel tries to make this easier to manage by:
-
Enabling extensions to return module_ctx.extension_metadata. This metadata specifies which repos generated by the extension that the root module must import via use_repo. This metadata supports the
"all"
keyword to insist that the root module import every generated repository. -
Providing the bazel mod tidy command. This command updates use_repo calls in
MODULE.bazel
automatically based onmodule_ctx.extension_metadata
values.
If your project actually references all these repositories directly in its own
BUILD
and .bzl
files, this is great. But if you want to use a rule set's
builtin toolchains, it's burdensome to have to import all of its dependencies
yourself. Even when using bazel mod tidy
, the resulting MODULE.bazel
file
becomes more verbose and difficult to comprehend. This is complicated further if
the names of the required repositories incorporate a version number (as with the
rules_scala
example above).
For example, try updating the EngFlow/example module extension at commit 79b5193 like so:
then run bazel build //scala/...
and bazel mod tidy
to see what happens.
This is what happens...
Basically, it will result in MODULE.bazel
importing many more repositories
than the build actually requires. The list comprehension containing
use_repo
runs over the repos
list to import the Scala version specific
repositories. Returning the extension_metadata
object with
root_module_direct_deps = "all"
forces the module to import all the non
version specific repos as well. All of these non version specific repos
contain aliases to the version specific repos, so they aren't required to
build at all. The repos
list was the result of trial and error, but it
produced a far less noisy MODULE.bazel
file.
Multiply this by however many projects that depend on rules_scala
, and by
similarly unmigrated repositories, and that's a lot of potential friction and
frustration.
The magic of "Toolchainization"¶
The Toolchainization technique described here removes all of the excess Bzlmod configuration complexity seen above, with only the minimum necessary configuration remaining. It involves having a module that defines toolchains:
-
provide a module extension to generate a repository containing its toolchain targets
-
instantiate all of the toolchain's dependency repositories from the same module extension
-
bring the toolchain repo into its own scope via
use_repo()
-
pass all the toolchain repo's targets to register_toolchains()
This way, all toolchains and their dependencies are encapsulated within the same scope, and the module registers the toolchains for you.
Due Credit
I developed this insight after studying the module extensions from rules_python and rules_go that generate toolchain repositories. Shortly after that, I noticed that Son Luong Ngoc suggested this exact approach on 2023-07-11.
For example, this is what the EngFlow/example
configuration looks like now
that rules_scala v7.0.0 is in the Bazel Central Registry:
This is a stripped down version of how the MODULE.bazel
file from
rules_scala
accomplishes this:
rules_scala registering its own toolchain repo | |
---|---|
The scala_deps
module extension from //scala/extensions:deps.bzl
performs
the magic here. The rest of this post will break down what's happening.
Toolchain repositories¶
The key to designing a well encapsulated toolchain is to generate a separate
toolchain repo using a repository_rule. In the case of rules_scala
, the
scala_deps
module extension from @rules_scala//scala/extensions:deps.bzl
generates @rules_scala_toolchains
.
As mentioned above, rules_scala
's builtin toolchains used to be defined in
BUILD
files in the rules_scala
repo itself. Under the legacy WORKSPACE
model, this worked because these targets and their dependencies in other
repositories shared the same global scope. This was the easiest, most
straightforward way to implement these toolchains at the time.
These same targets now reside in a repository generated by a repository_rule
invoked by a module extension. This unlocked the ability to generate their
dependency repos in the same module extension, and therefore the same scope.
The trick to accomplishing this for rules_scala
's toolchains was to define
three new components:
-
The scala_deps module extension, which defines a tag class to enable and/or configure each builtin toolchain.
-
The scala_toolchains macro, which
scala_deps
invokes to instantiate toolchain dependency repos based on values compiled from its tag classes. -
The scala_toolchains_repo repository rule, which
scala_toolchains
invokes to instantiate the toolchain repo after instantiating all the required dependency repos.
Together, these mechanisms produce the @rules_scala_toolchains
repo. The
Appendix: Implementation details section at
the very end contains a brief description of each component. But for now, let's
understand how rules_scala
automatically registers the toolchains from
@rules_scala_toolchains
, as this is crucial to the design.
register_toolchains()
and dynamic toolchain generation¶
One feature of the original legacy WORKSPACE
configuration schema was that
many of the builtin rules_scala
toolchains were optional. If you needed a
toolchain, you'd import one or more macros from a specific file into your legacy
WORKSPACE
file. These macros would instantiate dependency repos, then call
native.register_toolchains
to register that specific toolchain. For example:
Scalafmt and Scalatest legacy WORKSPACE configuration | |
---|---|
Under the new schema, we use a single module extension to generate all of the
builtin toolchains in @rules_scala_toolchains
. So the above configuration
looks like this under Bzlmod:
Scalafmt and Scalatest Bzlmod configuration | |
---|---|
We could've defined separate extensions to provide individual toolchains,
generating separate toolchain repos. But that proved unnecessary, and would've
made the register_toolchains
call in rules_scala
more complex.
Instead, scala_toolchains_repo
generates a variable number of packages based
on the configuration. scala_deps
compiles the toolchain options, passes them
to scala_toolchains
, and scala_toolchains_repo
generates packages for each
configured toolchain.
For example, here are the contents of the @rules_scala_toolchains
repo
generated from building rules_scala
itself:
Here are the contents of @rules_scala_toolchains
generated from building
EngFlow/example
(notice the longer canonical repo name):
This works because:
-
register_toolchains()
will register all toolchain targets in the specified target set, ignoring all other targets. The call still succeeds even if the set does not contain anytoolchain
targets, or is completely empty. -
The
@rules_scala_toolchains//...:all
specifier recursively discovers all packages present in the repository, and all targets within each package. This avoids the need to specify specific packages and targets, which can change based on the current toolchain configuration. -
rules_scala
always generates@rules_scala_toolchains
with an empty top levelBUILD
file, even if it contains no subpackages. This ensures that theregister_toolchains
call always succeeds, since@rules_scala_toolchains//...:all
will at least discover the empty root package.
Toolchain registration order¶
One of the improvements of Bzlmod over the legacy WORKSPACE
model is that
module registration is order independent. However, toolchain registration
remains somewhat order dependent. Bazel selects the first registered
toolchain matching the required toolchain_type
and other constraints defined
by the toolchain and rule targets.
Since modules can register toolchains themselves, the registration order becomes
more hierarchical, based on the structure of the module graph. Since the module
graph roughly implies a priority ordering of register_toolchain
calls,
toolchain registration generally works the right way.
Also, from Toolchain resolution:
Pseudo-targets like
:all
,:*
, and/...
are ordered by Bazel's package loading mechanism, which uses a lexicographic ordering.
This currently doesn't matter much for @rules_scala_toolchains
, since each
package registers a different toolchain_type
. The packages that register
multiple toolchains of the same type define a specific Scala version as a
constraint on each toolchain as well. (The introduction of user defined
toolchains into the rules_scala module extension API may have to take this
behavior into account, however.)
Root module toolchain registration and the dev_dependency
attribute¶
The root module retains these privileges when it comes to toolchain generation and registration:
-
Any toolchain registration in the root module (or on the command line) takes precedence over all other modules' toolchain registrations. This follows from the root module being at the top of the module graph hierarchy.
-
The 'dev_dependency' attribute of 'use_extension' activates certain module extension instances only when the module is the current root module. The 'dev_dependency' attribute of 'register_toolchains' does the same for toolchain registrations. (--ignore_dev_dependency disables both in the root module, too.)
These features provide Bazel modules the flexibility to specify which of its module extension instances and toolchain registrations apply to a given build.
These elements always apply to all builds that include the rules_scala
module:
-
rules_scala
invokes one instance of thescala_deps
module extension withoutdev_dependency = True
. Though this instance doesn't instantiate any tag classes, it ensures thatrules_scala
always generates the@rules_scala_toolchains
repository, even if it's empty. -
rules_scala
registers the toolchain repo without thedev_dependency = True
qualifier. This automatically registers all the toolchains in every package generated in that repository, as determined by the overall build configuration. This is a no-op if@rules_scala_toolchains
contains only the empty root package.
These elements only apply when building rules_scala
as the root module:
-
rules_scala
invokesuse_extension(..., dev_dependency = True)
on a different instance of thescala_deps
module extension. This instance instantiates all the tag classes, generating all the toolchains in@rules_scala_toolchains
required by its tests. -
rules_scala
also invokesregister_toolchains(..., dev_dependency = True)
on toolchain targets required specifically for its own testing. Thisregister_toolchains
call appears before the otherregister_toolchains
call to ensure thesedev_dependency
toolchains take precedence whenrules_scala
is the root module.
This explains why the rules_scala
instance of @rules_scala_toolchains
contains many more toolchains than the EngFlow/example
instance:
-
When
rules_scala
is the root module, it generates and registers all the toolchains described above. -
When
EngFlow/example
is the root module, Bazel does not generate and registerdev_dependency
toolchains from therules_scala
module. As a result,rules_scala
generates and registers only those toolchains specified by thescala_deps
extension instance in theEngFlow/example
module.
Compatibility between Bzlmod and legacy WORKSPACE
builds¶
One of the beautiful things about this toolchain repository schema is that it's
also compatible with legacy WORKSPACE
builds! In fact, I added
scala_toolchains
and scala_toolchains_repo
to rules_scala
before adding
the scala_deps
module extension. This guaranteed legacy WORKSPACE
compatibility at every step, to help users migrate to Bzlmod whenever they're
ready, rather than forcing an immediate migration.
Legacy WORKSPACE
files call scala_toolchains
directly; the scala_deps
module extension is essentially a thin layer on top of scala_toolchains
.
scala_deps
merely compiles tag class configuration values and passes them
through as arguments to scala_toolchains
. Not only was this good for code
reuse, but it yielded a few other important benefits:
-
The legacy WORKSPACE API surface shrank dramatically, replacing eighteen previous macros with
scala_toolchains
andscala_register_toolchains
. This also replaced up to eight or soload
statements with a singleload
to import these macros from@rules_scala//scala:toolchains.bzl
. -
Sharing
scala_toolchains
ensures that there are no differences between Bzlmod and legacyWORKSPACE
builds using equivalent configuration values. One can build with either the Bzlmod API or the legacyWORKSPACE
API and get the samerules_scala
behavior and the same results. -
The
scala_deps
andscala_toolchains
APIs are very similar, such that migrating legacyWORKSPACE
configurations toMODULE.bazel
should prove very easy.
So rules_scala v7.0.0 makes significant, breaking changes to the legacy
WORKSPACE
API, but still provides first class support to legacy
WORKSPACE
builds. Users that aren't yet ready to migrate to Bzlmod gain the
following benefits from adapting to the updated legacy WORKSPACE
API:
-
Their
rules_scala
configurations may shrink dramatically. -
They can build all rules_scala features using the latest releases of both Bazel 7 and Bazel 8, which wasn't possible before. (Bazel 6.5.0 will still work for now, but isn't officially supported.)
-
They're eased much closer towards Bzlmod adoption, given the similarity between Bzlmod and legacy
WORKSPACE
configurations, and the shared implementation underlying both.
Precompiled protocol buffer compiler (protoc
) toolchainization¶
Toolchainization isn't just for Bzlmod compatibility fixes! We encountered a
showstopping breakage of rules_scala
Windows MSVC builds when updating past
protobuf
v21.7. Toolchainizing prebuilt protoc
binaries by introducing
the @rules_scala_protoc_toolchains repo fixed this Windows breakage, and
improved performance across all platforms.
The error happened when compiling the protocol buffer compiler, protoc
, due to
a transitive dependency on @protobuf//:protoc
. The underlying causes were
that:
-
File paths in later versions of
protobuf
exceed Windows's dreaded 260 character path limit on rules_scala CI builds. It may be possible to enable long paths on Windows build machines, but MSVC apparently still enforces the 260 character limit. -
Most
rules_scala
tests are Bash scripts, which require the MSYS2 environment to run on Windows. MSYS2 runs within a home directory underC:/tools/msys64/home/
Updating all of therules_scala
test scripts to somehow switch between Bash and Windows batch files (or, worse, duplicating them) isn't feasible. (Setting --output_user_root toC:/b
for Windows builds might've been an option, but I didn't realize that until later.)
Compounding the problem is the fact that protobuf is in the process of dropping
MSVC support. That led me to investigate using a precompiled protoc
binary toolchain, such as toolchains_protoc. Bazel and protobuf
have
supported this via the --incompatible_enable_proto_toolchain_resolution
flag for some time (mostly, as we'll see). Though rather than use
toolchains_protoc
, I decided to implement the
@rules_scala_protoc_toolchains repo in the //protoc
package.
However, the build was still broken because rules_scala
still had targets
transitively depending on @protobuf//:protoc
(seen as
@com_google_protobuf//:protoc
below):
Fortunately, I found that protocolbuffers/protobuf#19679 already existed
to solve exactly this problem. It breaks the @protobuf//:protoc
dependency
when --incompatible_enable_proto_toolchain_resolution
is in effect. (I also
used details from this PR to create protoc/private/toolchain_impl.bzl and
update scala_proto_toolchain.) Unfortunately, the protobuf
maintainers
refuse to accept this change. They claim that
--incompatible_enable_proto_toolchain_resolution is deprecated, and the better
way isn't ready yet.
Immensely frustrating as this is, Bazel gives us a way to route around the
damage. I created a patch from protocolbuffers/protobuf#19679 and added it
to rules_scala
as //protoc:0001-protobuf-19679-rm-protoc-dep.patch
. Once I
wired up the patch and @rules_scala_protoc_toolchains as described in the
rules_scala README, we were back in business! And as a bonus, the entire
test suite ran much faster, shaving about 4 or 5 minutes off of CI builds.
Conclusion¶
After all of this talk of "toolchainization," I have to confess something. I could be inventing a word here, or ignorantly abusing an existing term (though if I am, no one's told me yet). Perhaps some helpful readers will gently correct my terminology if I'm indeed misusing it.
What I do know, however, is that the "toolchainized" design of rules_scala
v7.0.0 does The Right Thing. It enables Bzlmod compatibility,
reduces the legacy WORKSPACE
API without losing functionality, and makes it
easy to migrate to Bzlmod in the near future. It also unlocked a path around a
serious protoc
build breakage, with significant performance benefits to boot.
I also know it would've been impossible to make such consequential changes
without tests! I'm very grateful that rules_scala
already had an extensive
test suite that gave me the utmost confidence that my changes were correct. I
found it easy to add new test suites for the new Bzlmod helper functions, for
MODULE.bazel
linting, and for validating version compatibility.
I'm equally as grateful for the wonderful support of the rules_scala
maintainers, Simonas Pinevičius and Vaidas Pilkauskas, and the
rules_scala community at large. Gergely Fábián, in
particular, gave me several bits of helpful feedback after trying my changes
along the way (including documentation updates!). Thanks to Fabian Meumertzheim
for protocolbuffers/protobuf#19679, in addition to all the help with repo
names. Thanks to Yun Peng and Alex Eagle for helping get the rules_scala module
published to the BCR. Thanks to Jay Conrod for nerd sniping me into
starting the rules_scala
Bzlmodification project. And, of course, thanks to
EngFlow for allowing me to contribute substantially to an important Open Source
project.
So what's next? As far as rules_scala
is concerned, I'll continue
experimenting with adding user defined toolchains to its toolchain registration
interface. I'll likely continue to submit occasional dependency updates as
well.
As far as this blog series is concerned, I haven't forgotten about the topic I mentioned last time:
However, I think I'll first write about testing to ensure compatibility with
minimum dependency versions, while still testing against the latest dependency
versions. A conversation Simon Stewart initiated in the #bzlmod channel of the
Bazel Slack workspace on 2025-04-02 inspired me to do this for
rules_scala
.
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!
Appendix: Implementation details¶
Here's a very brief overview of the new components supporting the toolchainization of rules_scala v7.0.0. The description of each component contains a link to its implementation; open these links to have the implementations handy while reading.
scala_toolchains_repo
¶
scala_toolchains_repo is a repository rule that takes a number of
attributes pertaining to the different toolchains built into rules_scala
. It
compiles these parameters and prepares them for injection into BUILD
files
that define the packages for each enabled toolchain.
It also injects the canonical repo name of @rules_scala
itself into
these BUILD
files, so they can load
toolchain macros from @rules_scala
.
These are the same toolchain macros currently available to users for defining
their own custom toolchains.
scala_toolchains
¶
scala_toolchains wraps the call to scala_toolchains_repo
, invoking it
after instantiating all of the repositories required by every configured
toolchain. It also exposes a more flexible, user friendly interface than the
attributes exposed by scala_toolchains_repo
.
Notice that each of the toolchains has a corresponding *_artifact_ids()
macro.
This is part of the dependency schema defined by the scala_*.bzl
files in
third_party/repositories. These files define repositories for Maven
artifacts that are compatible with each supported Scala version, where the repo
name is the "artifact id". rules_scala
doesn't use rules_jvm_external
(yet), so the *_artifact_ids()
macros specify the dependencies required
by each builtin toolchain.
The internal design of scala_toolchains
solves a Bzlmod compatibility problem.
Instantiating a repo multiple times works under legacy WORKSPACE
builds, but
breaks under Bzlmod. Since some toolchains share some of the same dependencies,
scala_toolchains
dedupes the results from these macros to instantiate each
repo only once. It does so by building dictionaries that use the repository
labels returned by the *_artifact_ids()
macros as keys.
These dictionaries map toolchain dependency repo names to a boolean indicating
whether or not to download the repo's sources. The only purpose of the
fetch_sources
boolean is to preserve the default behavior for each toolchain
from v6.6.0
. The more important detail is that the dictionaries build lists of
toolchain dependencies for each configured Scala version to pass to
repositories.
scala_deps
¶
The scala_deps module extension, which ultimately calls
scala_toolchains
, defines a tag class to enable and/or configure each
builtin toolchain. It's a thin layer that translates tag class values to
scala_toolchains
parameters. However, it provides type checked arguments and
well defined evaluation of configurations across modules. This yields
convenience and consistency that the legacy WORKSPACE
model can't provide.
The _toolchains_settings function enables and configures the builtin toolchains specified throughout all modules in the module graph. It implements the policy that any module can enable a builtin toolchain, but only the root module can customize its configuration. Otherwise a builtin toolchain receives its default configuration values.
Also, only the overridden_artifacts
, compiler_srcjars
, and settings
tags
from the root module take effect. Otherwise the first two sets of tag class
values remain empty, and the defaults for settings
apply.
This is similar to how configuration macro calls in legacy WORKSPACE
builds
work, but is explicit and easy to reason about. Unlike legacy WORKSPACE
builds, the extension automatically instantiates toolchains required by the
build that aren't directly referenced by the main repository.
To reduce duplication and conditional logic, I defined a set of common
implementation functions in scala/private/macros/bzlmod.bzl. As explained
at the top of that file, they're based on the pattern of defining default values
for tag class attrs in a separate dictionary. This enables module
extension implementations to implement logic based on these default values,
which are not directly accessible from attr
objects. This, in turn, removes
the need for logic to check whether values are present or that they match
hardcoded, likely duplicated values. The test/shell/test_bzlmod_macros.sh
test script tests these functions in isolation from the scala_deps
module
extension. (I'm thinking about moving _toolchain_settings
into that file as
well.)
Thanks to these common functions, it's clear that scala_deps really is just a thin wrapper on top of scala_toolchains: