Skip to content

Commit 48dfa91

Browse files
authored
Add toolchain options API to WORKSPACE and Bzlmod (#1730)
* Add toolchain options API to WORKSPACE and Bzlmod Updates `scala_toolchains()` to accept either boolean or dict arguments for specific toolchains, and updates `//scala/extensions:deps.bzl` to generate them from tag classes. Part of #1482. Notable qualities: - Adds toolchain options support to the `scala_toolchains()` parameters `scalafmt`, `scala_proto`, and `twitter_scrooge`, and to the `scalafmt` tag class. - Eliminates the `scalafmt_default_config`, `scala_proto_options`, and `twitter_scrooge_deps` option parameters from `scala_toolchains()`. - Provides uniform, strict evaluation and validation of toolchain options passed to `scala_toolchains()`. - Configures enabled toolchains using root module settings or the default toolchain settings only. - Introduces the shared `TOOLCHAIN_DEFAULTS` dict in `//scala/private:toolchains_defaults.bzl` to aggregate individual `TOOLCHAIN_DEFAULTS` macro parameter dicts. This change also: - Replaces the non-dev dependency `scala_deps.scala()` tag instantiation in `MODULE.bazel` with `dev_deps.scala()`. - Renames the `options` parameter of the `scala_deps.scala_proto` tag class to `default_gen_opts` to match `setup_scala_proto_toolchain()`. - Introduces `_stringify_args()` to easily pass all toolchain macro args compiled from `scala_toolchains_repo()` attributes through to the generated `BUILD` file. - Extracts `_DEFAULT_TOOLCHAINS_REPO_NAME` and removes the `scala_toolchains_repo()` macro. - Includes docstrings for the new private implementation functions, and updates all other docstrings, `README.md`, and other relevant documentation accordingly. --- Inspired by @simuons's suggestion to replace toolchain macros with a module extension in: - #1722 (comment) Though a full replacement is a ways off, this is a step in that direction that surfaced several immediate improvements. First, extensibility and maintainability: - The new implementation enables adding options support for other toolchains in the future while maintaining backward compatibility, for both the `WORKSPACE` and Bzlmod APIs. Adding this support will only require a minor release, not a major one. - The `scala_deps` module extension implementation is easier to read, since all toolchains now share the `_toolchain_settings` mechanism. Next, improved consistency of the API and implementation: - Toolchain options parameters should present all the same parameters as the macros to which they correspond, ensured by the `TOOLCHAIN_DEFAULTS` mechanism. This is to make it easier for users and maintainers to see the direct relationship between these separate sets of parameters. (They can also define additional parameters to those required by the macro, like `default_config` from the `scalafmt` options.) This principle drove the renaming of the `scala_deps.scala_proto` tag class parameter from `options` to `default_gen_opts`. It also inspired updating `scala_toolchains_repo()` to pass toolchain options through `_stringify_args()` to generate `BUILD` macro arguments. - The consolidated `TOOLCHAIN_DEFAULTS` dict reduces duplication between the `scala/extensions/deps.bzl` and `scala/toolchains.bzl` files. It ensures consistency between tag class `attr` default values for Bzlmod users and the `scala_toolchains()` default parameter values for `WORKSPACE` users. The `TOOLCHAINS_DEFAULTS` dicts corresponding to each toolchain macro do duplicate the information in the macro argument lists. However, the duplicated values in this case are physically adjacent to one another, minimizing the risk of drift. - Extracting `_DEFAULT_TOOLCHAINS_REPO_NAME` is a step towards enabling customized repositories based on the builtin toolchains, while specifying different options. This extraction revealed the fact that the `scala_toolchains_repo()` macro was no longer necessary, since only `scala_toolchains()` ever called it. Finally, fixes for the following design bugs: - Previously, `scala_deps.scala_proto(options = [...])` compiled the list of `default_gen_opts` from all tag instances in the module graph. This might've been convenient, but didn't generalize to other options for other toolchains. In particular, it differed from the previous `toolchains`, `scalafmt`, and `twitter_scrooge` tag class behavior. The new semantics are unambiguous, consistent, and apply to all toolchains equally; they do not show a preference for any one toolchain over the others. They also maintain the existing `scalafmt` and `twitter_scrooge` tag class semantics, but now using a generic mechanism, simplifying the implementation. - Instantating `scala_deps.scala()` was a bug left over from the decision in #1722 _not_ to enable the builtin Scala toolchain by default under Bzlmod. The previous `scala_deps.toolchains()` tag class had a default `scala = True` parameter. The user could set `scala = False` to disable the builtin Scala toolchain. After replacing `toolchains()` with individual tag classes, the documented behavior was that the user must enable the builtin Scala toolchain by instantiating `scala_deps.scala()`. By instantiating `scala_deps.scala()` in our own `MODULE.bazel` file, we ensured that `rules_scala` would always instantiate the builtin Scala toolchain. While relatively harmless, it violated the intention of allowing the user to avoid instantiating the toolchain altogether. * Update documentation for toolchain option dicts Touched up documentation for the `scalafmt` and `scala_proto` toolchains. * Replace Scalafmt default_config alias with symlink This avoids the need for the user to use `exports_files` so `@rules_scala_toolchains//scalafmt:config` can access the config file. Essentially restores the API from before #1725, but still fixes the same bug as #1725. * Update phase_scalafmt.md, check scalafmt conf path Updates the Scalafmt documentation to reflect the current API. Adds a check to `scala_toolchains_repo` to `fail` if the Scalafmt `default_config` file doesn't exist. The previous commit doesn't actually restore the exact pre-#1725 API. It eliminates the `exports_files` requirement, but still requires a `Label` or a relative path string, not an optional `.scalafmt.conf` in the root directory. After experimenting a bit and thinking this through, dropping support for an optional `.scalafmt.conf` provides the most robust and reliable interface. Specifically, supporting it requires detecting whether it actually exists before falling back to the default. Having users explicitly specify their own config seems a small burden to impose for a more straightforward and correct implementation. At the same time, I saw the opportunity to provide the user with explicit feedback if the specified config file doesn't exist. Hence the new check and `fail()` message. Also renamed the generated `.scalafmt.conf` file in `@rules_scala_toolchains//scalafmt` to `scalafmt.conf`. No need for it to be a hidden file in that context. * Update the `scala_deps` module extension docstring The `scala_deps` docstring now more accurately describes the behavior implemented by `_toolchain_settings()`. * Fix `scala_proto` param in test_version/WORKSPACE Caught this after doing an `--enable_workspace --noenable_bzlmod` build.
1 parent e2575a3 commit 48dfa91

15 files changed

+273
-177
lines changed

BUILD

-1
Original file line numberDiff line numberDiff line change
@@ -1 +0,0 @@
1-
exports_files([".scalafmt.conf"])

MODULE.bazel

+1-1
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,6 @@ use_repo(
9595
"rules_scala_toolchains",
9696
"scala_compiler_sources",
9797
)
98-
scala_deps.scala()
9998

10099
# Register some of our testing toolchains first when building our repo.
101100
register_toolchains(
@@ -119,6 +118,7 @@ dev_deps = use_extension(
119118
"scala_deps",
120119
dev_dependency = True,
121120
)
121+
dev_deps.scala()
122122
dev_deps.jmh()
123123
dev_deps.junit()
124124
dev_deps.scala_proto()

README.md

+23-6
Original file line numberDiff line numberDiff line change
@@ -930,19 +930,36 @@ parameter list, which is almost in complete correspondence with parameters from
930930
the previous macros. The `WORKSPACE` files in this repository also provide many
931931
examples.
932932

933-
### Replacing toolchain registration macros in `WORKSPACE`
933+
### Replacing toolchain registration macros
934934

935935
Almost all `rules_scala` toolchains configured using `scala_toolchains()` are
936-
automatically registered by `scala_register_toolchains()`. There are two
937-
toolchain macro replacements that require special handling.
936+
automatically registered by `scala_register_toolchains()`. The same is true for
937+
toolchains configured using the `scala_deps` module extension under Bzlmod.
938+
There are two toolchain macro replacements that require special handling.
938939

939940
The first is replacing `scala_proto_register_enable_all_options_toolchain()`
940-
with the following `scala_toolchains()` parameters:
941+
with the following:
941942

942943
```py
944+
# MODULE.bazel
945+
946+
scala_deps.scala_proto(
947+
"default_gen_opts" = [
948+
"flat_package",
949+
"grpc",
950+
"single_line_to_proto_string",
951+
],
952+
)
953+
954+
# WORKSPACE
943955
scala_toolchains(
944-
scala_proto = True,
945-
scala_proto_options = [],
956+
scala_proto = {
957+
"default_gen_opts": [
958+
"flat_package",
959+
"grpc",
960+
"single_line_to_proto_string",
961+
],
962+
},
946963
)
947964
```
948965

docs/phase_scalafmt.md

+15-19
Original file line numberDiff line numberDiff line change
@@ -71,25 +71,21 @@ bazel run <TARGET>.format-test
7171

7272
to check the format (without modifying source code).
7373

74-
The extension provides a default configuration, but there are 2 ways to use
75-
a custom configuration:
76-
77-
- Put `.scalafmt.conf` at the root of your workspace
78-
- Pass `.scalafmt.conf` in via `scala_toolchains`:
79-
80-
```py
81-
# MODULE.bazel
82-
scala_deps.scalafmt(
83-
default_config = "//path/to/my/custom:scalafmt.conf",
84-
)
85-
86-
# WORKSPACE
87-
scala_toolchains(
88-
# Other toolchains settings...
89-
scalafmt = True,
90-
scalafmt_default_config = "//path/to/my/custom:scalafmt.conf",
91-
)
92-
```
74+
The extension provides a default configuration. To use a custom configuration,
75+
pass its path or target Label to the toolchain configuration:
76+
77+
```py
78+
# MODULE.bazel
79+
scala_deps.scalafmt(
80+
default_config = "path/to/my/custom/scalafmt.conf",
81+
)
82+
83+
# WORKSPACE
84+
scala_toolchains(
85+
# Other toolchains settings...
86+
scalafmt = {"default_config": "path/to/my/custom/scalafmt.conf"},
87+
)
88+
```
9389

9490
When using Scala 3, you must append `runner.dialect = scala3` to
9591
`.scalafmt.conf`.

docs/scala_proto_library.md

+24-6
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,40 @@
11
# scala_proto_library
22

3-
To use this rule, you'll first need to add the following to your `WORKSPACE` file,
4-
which adds a few dependencies needed for ScalaPB:
3+
To use this rule, add the following configuration, which adds the dependencies
4+
needed for ScalaPB:
55

66
```py
7-
scala_toolchains(
8-
# Other toolchains settings...
9-
scala_proto = True,
10-
scala_proto_options = [
7+
# MODULE.bazel
8+
scala_deps.scala_proto(
9+
"default_gen_opts" = [
1110
"grpc",
1211
"flat_package",
1312
"scala3_sources",
1413
],
1514
)
15+
```
16+
17+
```py
18+
# WORKSPACE
19+
scala_toolchains(
20+
# Other toolchains settings...
21+
scala_proto = {
22+
"default_gen_opts": [
23+
"grpc",
24+
"flat_package",
25+
"scala3_sources",
26+
],
27+
},
28+
)
1629

1730
scala_register_toolchains()
1831
```
1932

33+
See the __scalapbc__ column of the [__ScalaPB: SBT Settings > Additional options
34+
to the generator__](
35+
https://scalapb.github.io/docs/sbt-settings#additional-options-to-the-generator
36+
) table for `default_gen_opts` values.
37+
2038
Then you can import `scala_proto_library` in any `BUILD` file like this:
2139

2240
```py

scala/extensions/deps.bzl

+51-48
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ Provides the `scala_deps` module extension with the following tag classes:
1414
- `twitter_scrooge`
1515
- `jmh`
1616
17-
For documentation, see the `_tag_classes` dict, and the `_<TAG>_attrs` dict
18-
corresponding to each `<TAG>` listed above.
17+
For documentation, see the `_{general,toolchain}_tag_classes` dicts and the
18+
`_<TAG>_attrs` dict corresponding to each `<TAG>` listed above.
1919
2020
See the `scala/private/macros/bzlmod.bzl` docstring for a description of
2121
the defaults, attrs, and tag class dictionaries pattern employed here.
@@ -27,6 +27,7 @@ load(
2727
"root_module_tags",
2828
"single_tag_values",
2929
)
30+
load("//scala/private:toolchain_defaults.bzl", "TOOLCHAIN_DEFAULTS")
3031
load("//scala:scala_cross_version.bzl", "default_maven_server_urls")
3132
load("//scala:toolchains.bzl", "scala_toolchains")
3233

@@ -89,35 +90,26 @@ _compiler_srcjar_attrs = {
8990
"integrity": attr.string(),
9091
}
9192

92-
_scalafmt_defaults = {
93-
"default_config": "//:.scalafmt.conf",
94-
}
93+
_scalafmt_defaults = TOOLCHAIN_DEFAULTS["scalafmt"]
9594

9695
_scalafmt_attrs = {
9796
"default_config": attr.label(
9897
default = _scalafmt_defaults["default_config"],
9998
doc = "The default config file for Scalafmt targets",
99+
allow_single_file = True,
100100
),
101101
}
102102

103-
_scala_proto_defaults = {
104-
"options": [],
105-
}
103+
_scala_proto_defaults = TOOLCHAIN_DEFAULTS["scala_proto"]
106104

107105
_scala_proto_attrs = {
108-
"options": attr.string_list(
109-
default = _scala_proto_defaults["options"],
106+
"default_gen_opts": attr.string_list(
107+
default = _scala_proto_defaults["default_gen_opts"],
110108
doc = "Protobuf options, like 'scala3_sources' or 'grpc'",
111109
),
112110
}
113111

114-
_twitter_scrooge_defaults = {
115-
"libthrift": None,
116-
"scrooge_core": None,
117-
"scrooge_generator": None,
118-
"util_core": None,
119-
"util_logging": None,
120-
}
112+
_twitter_scrooge_defaults = TOOLCHAIN_DEFAULTS["twitter_scrooge"]
121113

122114
_twitter_scrooge_attrs = {
123115
k: attr.label(default = v)
@@ -186,39 +178,51 @@ _toolchain_tag_classes = {
186178
),
187179
}
188180

189-
_tag_classes = _general_tag_classes | _toolchain_tag_classes
181+
def _toolchain_settings(module_ctx, tags, tc_names, toolchain_defaults):
182+
"""Configures all builtin toolchains enabled throughout the module graph.
183+
184+
Configures toolchain options for enabled toolchains that support them based
185+
on the root module's settings for each toolchain. In other words, it uses:
190186
191-
def _toolchains(mctx):
192-
result = {k: False for k in _toolchain_tag_classes}
187+
- the root module's tag class settings, if present; and
188+
- the default tag class settings otherwise.
193189
194-
for mod in mctx.modules:
195-
values = {tc: len(getattr(mod.tags, tc)) != 0 for tc in result}
190+
This avoids trying to reconcile different toolchain settings across the
191+
module graph. Non root modules that require specific settings should either:
196192
197-
if mod.is_root:
198-
return values
193+
- publish their required toolchain settings, or
194+
- define and register a custom toolchain instead.
199195
200-
# Don't overwrite `True` values with `False` from another tag.
201-
result.update({k: v for k, v in values.items() if v})
196+
Args:
197+
module_ctx: the module context object
198+
tags: a tags object, presumably the result of `root_module_tags()`
199+
tc_names: names of all supported toolchains
200+
toolchain_defaults: a dict of `{toolchain_name: default options dict}`
202201
203-
return result
202+
Returns:
203+
a dict of `{toolchain_name: bool or options dict}` to pass as keyword
204+
arguments to `scala_toolchains()`
205+
"""
206+
toolchains = {k: False for k in tc_names}
204207

205-
def _scala_proto_options(mctx):
206-
result = {}
208+
for mod in module_ctx.modules:
209+
values = {tc: len(getattr(mod.tags, tc)) != 0 for tc in toolchains}
207210

208-
for mod in mctx.modules:
209-
for tag in mod.tags.scala_proto:
210-
result.update({opt: True for opt in tag.options})
211+
# Don't overwrite True values with False from another tag.
212+
toolchains.update({k: v for k, v in values.items() if v})
211213

212-
return sorted(result.keys())
214+
for tc, defaults in toolchain_defaults.items():
215+
if toolchains[tc]:
216+
values = single_tag_values(module_ctx, getattr(tags, tc), defaults)
217+
toolchains[tc] = {k: v for k, v in values.items() if v != None}
218+
219+
return toolchains
220+
221+
_tag_classes = _general_tag_classes | _toolchain_tag_classes
213222

214223
def _scala_deps_impl(module_ctx):
215224
tags = root_module_tags(module_ctx, _tag_classes.keys())
216-
scalafmt = single_tag_values(module_ctx, tags.scalafmt, _scalafmt_defaults)
217-
scrooge_deps = single_tag_values(
218-
module_ctx,
219-
tags.twitter_scrooge,
220-
_twitter_scrooge_defaults,
221-
)
225+
tc_names = [tc for tc in _toolchain_tag_classes]
222226

223227
scala_toolchains(
224228
overridden_artifacts = repeated_tag_values(
@@ -229,13 +233,9 @@ def _scala_deps_impl(module_ctx):
229233
tags.compiler_srcjar,
230234
_compiler_srcjar_attrs,
231235
),
232-
scala_proto_options = _scala_proto_options(module_ctx),
233-
# `None` breaks the `attr.string_dict` in `scala_toolchains_repo`.
234-
twitter_scrooge_deps = {k: v for k, v in scrooge_deps.items() if v},
235236
**(
236237
single_tag_values(module_ctx, tags.settings, _settings_defaults) |
237-
{"scalafmt_%s" % k: v for k, v in scalafmt.items()} |
238-
_toolchains(module_ctx)
238+
_toolchain_settings(module_ctx, tags, tc_names, TOOLCHAIN_DEFAULTS)
239239
)
240240
)
241241

@@ -244,23 +244,26 @@ scala_deps = module_extension(
244244
tag_classes = _tag_classes,
245245
doc = """Selects and configures builtin toolchains.
246246
247-
If the root module explicitly uses the extension, it assumes responsibility for
248-
selecting all required toolchains by insantiating the corresponding tag classes:
247+
Modules throughout the dependency graph can enable a builtin toolchain by
248+
instantiating its corresponding tag class. The root module controls the
249+
configuration of all toolchains it directly enables. Any other builtin
250+
toolchain required by other modules will use that toolchain's default
251+
configuration values.
249252
250253
```py
251254
scala_deps = use_extension(
252255
"@rules_scala//scala/extensions:deps.bzl",
253256
"scala_deps",
254257
)
255258
scala_deps.scala()
256-
scala_deps.scala_proto()
259+
scala_deps.scala_proto(default_gen_opts = ["grpc", "scala3_sources"])
257260
258261
dev_deps = use_extension(
259262
"@rules_scala//scala/extensions:deps.bzl",
260263
"scala_deps",
261264
dev_dependency = True,
262265
)
263-
dev_deps.scalafmt()
266+
dev_deps.scalafmt(default_config = "path/to/scalafmt.conf")
264267
dev_deps.scalatest()
265268
266269
# And so on...

scala/private/toolchain_defaults.bzl

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
"""Gathers defaults for toolchain macros in one place.
2+
3+
Used by both //scala:toolchains.bzl and //scala/extensions:deps.bzl.
4+
"""
5+
6+
load(
7+
"//scala/scalafmt/toolchain:setup_scalafmt_toolchain.bzl",
8+
_scalafmt = "TOOLCHAIN_DEFAULTS",
9+
)
10+
load("//scala_proto:toolchains.bzl", _scala_proto = "TOOLCHAIN_DEFAULTS")
11+
load(
12+
"//twitter_scrooge/toolchain:toolchain.bzl",
13+
_twitter_scrooge = "TOOLCHAIN_DEFAULTS",
14+
)
15+
16+
TOOLCHAIN_DEFAULTS = {
17+
"scalafmt": _scalafmt,
18+
"scala_proto": _scala_proto,
19+
"twitter_scrooge": _twitter_scrooge,
20+
}

scala/scalafmt/toolchain/setup_scalafmt_toolchain.bzl

+7
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@ load("//scala:providers.bzl", "declare_deps_provider")
88
load("//scala:scala_cross_version.bzl", "version_suffix")
99
load("@rules_scala_config//:config.bzl", "SCALA_VERSIONS")
1010

11+
TOOLCHAIN_DEFAULTS = {
12+
# Used by `scala_toolchains{,_repo}` to generate
13+
# `@rules_scala_toolchains//scalafmt:config`, the default config for
14+
# `ext_scalafmt` from `phase_scalafmt_ext.bzl`.
15+
"default_config": Label("//:.scalafmt.conf"),
16+
}
17+
1118
def setup_scalafmt_toolchain(
1219
name,
1320
scalafmt_classpath,

0 commit comments

Comments
 (0)