Skip to content

Commit 937d469

Browse files
borkaehwittaiz
authored andcommitted
Refactor rules into configurable phases (#865)
* Add configurable phases * Refactor rules implementation into configurable phases * Customizable phases * Customizable phases tests * Break up init to more reasonable phases * Move final to non-configurable phase * Rename parameter builtin_customizable_phases * Fix ijar * Switch default for buildijar * Add TODOs * Rename provider * Move to advanced_usage * rename custom_phases * Make default phase private * Fix exports_jars * Adjusted_phases * Rename p to be more clear * Add in-line comments * Fix lint * Add doc for phases * Doc for consumers * Doc for contributors * Add more content * Fix md * Test for all rules * Fix junit test * Fix lint * Add more tests * Fix junit test * Fix doc * Change _test_ to _scalatest_ * More doc on provider
1 parent cfff088 commit 937d469

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1866
-619
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,16 @@ Unused dependency checking can either be enabled globally for all targets using
189189
in these cases you can enable unused dependency checking globally through a toolchain and override individual misbehaving targets
190190
using the attribute.
191191

192+
## Advanced configurable rules
193+
To make the ruleset more flexible and configurable, we introduce a phase architecture. By using a phase architecture, where rule implementations are defined as a list of phases that are executed sequentially, functionality can easily be added (or modified) by adding (or swapping) phases.
194+
195+
Phases provide 3 major benefits:
196+
- Consumers are able to configure the rules to their specific use cases by defining new phases within their workspace without impacting other consumers.
197+
- Contributors are able to implement new functionalities by creating additional default phases.
198+
- Phases give us more clear idea what steps are shared across rules.
199+
200+
See [Customizable Phase](docs/customizable_phase.md) for more info.
201+
192202
## Building from source
193203
Test & Build:
194204
```

WORKSPACE

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@ workspace(name = "io_bazel_rules_scala")
33
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
44
load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")
55
load("@bazel_tools//tools/build_defs/repo:jvm.bzl", "jvm_maven_import_external")
6+
7+
http_archive(
8+
name = "com_github_bazelbuild_buildtools",
9+
sha256 = "cdaac537b56375f658179ee2f27813cac19542443f4722b6730d84e4125355e6",
10+
strip_prefix = "buildtools-f27d1753c8b3210d9e87cdc9c45bc2739ae2c2db",
11+
url = "https://github.com/bazelbuild/buildtools/archive/f27d1753c8b3210d9e87cdc9c45bc2739ae2c2db.zip",
12+
)
13+
14+
load("@com_github_bazelbuild_buildtools//buildifier:deps.bzl", "buildifier_dependencies")
15+
16+
buildifier_dependencies()
17+
618
load("//scala:scala.bzl", "scala_repositories")
719

820
scala_repositories()
@@ -162,13 +174,6 @@ http_archive(
162174
url = "https://github.com/bazelbuild/rules_go/releases/download/0.18.7/rules_go-0.18.7.tar.gz",
163175
)
164176

165-
http_archive(
166-
name = "com_github_bazelbuild_buildtools",
167-
sha256 = "cdaac537b56375f658179ee2f27813cac19542443f4722b6730d84e4125355e6",
168-
strip_prefix = "buildtools-f27d1753c8b3210d9e87cdc9c45bc2739ae2c2db",
169-
url = "https://github.com/bazelbuild/buildtools/archive/f27d1753c8b3210d9e87cdc9c45bc2739ae2c2db.zip",
170-
)
171-
172177
load(
173178
"@io_bazel_rules_go//go:deps.bzl",
174179
"go_register_toolchains",
@@ -179,10 +184,6 @@ go_rules_dependencies()
179184

180185
go_register_toolchains()
181186

182-
load("@com_github_bazelbuild_buildtools//buildifier:deps.bzl", "buildifier_dependencies")
183-
184-
buildifier_dependencies()
185-
186187
http_archive(
187188
name = "bazel_toolchains",
188189
sha256 = "5962fe677a43226c409316fcb321d668fc4b7fa97cb1f9ef45e7dc2676097b26",

docs/customizable_phase.md

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
# Customizable Phase
2+
3+
## Contents
4+
* [Overview](#overview)
5+
* [Who needs customizable phase](#who-needs-customizable-phase)
6+
* [As a consumer](#as-a-consumer)
7+
* [As a contributor](#as-a-contributor)
8+
* [Phase naming convention](#phase-naming-convention)
9+
10+
## Overview
11+
Phases increase configurability. Rule implementations are defined as a list of phases. Each phase defines a specific step, which helps breaking up implementation into smaller and more readable groups. Some phases are independent from others, which means the order doesn't matter. However, some phases depend on outputs of previous phases, in this case, we should make sure it meets all the prerequisites before executing phases.
12+
13+
The biggest benefit of phases is that it is customizable. If default phase A is not doing what you expect, you may switch it with your self-defined phase A. One use case is to write your own compilation phase with your favorite Scala compiler. You may also extend the default phase list for more functionalities. One use case is to check the Scala format.
14+
15+
## Who needs customizable phase
16+
Customizable phase is an advanced feature for people who want the rules to do more. If you are an experienced Bazel rules developer, we make this powerful API public for you to do custom work without impacting other consumers. If you have no experience on writing Bazel rules, we are happy to help but be aware it may be frustrating at first.
17+
18+
If you don't need to customize your rules and just need the default setup to work correctly, then just load the following file for default rules:
19+
```
20+
load("@io_bazel_rules_scala//scala:scala.bzl")
21+
```
22+
Otherwise read on:
23+
24+
## As a consumer
25+
You need to load the following 2 files:
26+
```
27+
load("@io_bazel_rules_scala//scala:advanced_usage/providers.bzl", "ScalaRulePhase")
28+
load("@io_bazel_rules_scala//scala:advanced_usage/scala.bzl", "make_scala_binary")
29+
```
30+
`ScalaRulePhase` is a phase provider to pass in custom phases. Rules with `make_` prefix, like `make_scala_binary`, are customizable rules. `make_<RULE_NAME>`s take a dictionary as input. It currently supports appending `attrs` and `outputs` to default rules, as well as modifying the phase list.
31+
32+
For example:
33+
```
34+
ext_add_custom_phase = {
35+
"attrs": {
36+
"custom_content": attr.string(
37+
default = "This is custom content",
38+
),
39+
},
40+
"outputs": {
41+
"custom_output": "%{name}.custom-output",
42+
},
43+
"phase_providers": [
44+
"//custom/phase:phase_custom_write_extra_file",
45+
],
46+
}
47+
48+
custom_scala_binary = make_scala_binary(ext_add_custom_phase)
49+
```
50+
`make_<RULE_NAME>`s append `attrs` and `outputs` to the default rule definitions. All items in `attrs` can be accessed by `ctx.attr`, and all items in `outputs` can be accessed by `ctx.outputs`. `phase_providers` takes a list of targets which define how you want to modify phase list.
51+
```
52+
def _add_custom_phase_singleton_implementation(ctx):
53+
return [
54+
ScalaRulePhase(
55+
custom_phases = [
56+
("last", "", "custom_write_extra_file", phase_custom_write_extra_file),
57+
],
58+
),
59+
]
60+
61+
add_custom_phase_singleton = rule(
62+
implementation = _add_custom_phase_singleton_implementation,
63+
)
64+
```
65+
`add_custom_phase_singleton` is a rule solely to pass in custom phases using `ScalaRulePhase`. The `custom_phases` field in `ScalaRulePhase` takes a list of tuples. Each tuple has 4 elements:
66+
```
67+
(relation, peer_name, phase_name, phase_function)
68+
```
69+
- relation: the position to add a new phase
70+
- peer_name: the existing phase to compare the position with
71+
- phase_name: the name of the new phase, also used to access phase information
72+
- phase_function: the function of the new phase
73+
74+
There are 5 possible relations:
75+
- `^` or `first`
76+
- `$` or `last`
77+
- `-` or `before`
78+
- `+` or `after`
79+
- `=` or `replace`
80+
81+
The symbols and words are interchangable. If `first` or `last` is used, it puts your custom phase at the beginning or the end of the phase list, `peer_name` is not needed.
82+
83+
Then you have to call the rule in a `BUILD`
84+
```
85+
add_custom_phase_singleton(
86+
name = "phase_custom_write_extra_file",
87+
visibility = ["//visibility:public"],
88+
)
89+
```
90+
91+
You may now see `phase_providers` in `ext_add_custom_phase` is pointing to this target.
92+
93+
The last step is to write the function of the phase. For example:
94+
```
95+
def phase_custom_write_extra_file(ctx, p):
96+
ctx.actions.write(
97+
output = ctx.outputs.custom_output,
98+
content = ctx.attr.custom_content,
99+
)
100+
```
101+
Every phase has 2 arguments, `ctx` and `p`. `ctx` gives you access to the fields defined in rules. `p` is the global provider, which contains information from initial state as well as all the previous phases. You may access the information from previous phases by `p.<PHASE_NAME>.<FIELD_NAME>`. For example, if the previous phase, said `phase_jar` with phase name `jar`, returns a struct
102+
```
103+
def phase_jar(ctx, p):
104+
# Some works to get the jars
105+
return struct(
106+
class_jar = class_jar,
107+
ijar = ijar,
108+
)
109+
```
110+
You are able to access information like `p.jar.class_jar` in `phase_custom_write_extra_file`. You can provide the information for later phases in the same way, then they can access it by `p.custom_write_extra_file.<FIELD_NAME>`.
111+
112+
You should be able to define the files above entirely in your own workspace without making change to the [bazelbuild/rules_scala](https://github.com/bazelbuild/rules_scala). If you believe your custom phase will be valuable to the community, please refer to [As a contributor](#as-a-contributor). Pull requests are welcome.
113+
114+
## As a contributor
115+
Besides the basics in [As a consumer](#as-a-consumer), the followings help you understand how phases are setup if you plan to contribute to [bazelbuild/rules_scala](https://github.com/bazelbuild/rules_scala).
116+
117+
These are the relevant files
118+
- `scala/private/phases/api.bzl`: the API of executing and modifying the phase list
119+
- `scala/private/phases/phases.bzl`: re-expose phases for convenience
120+
- `scala/private/phases/phase_<PHASE_NAME>.bzl`: all the phase definitions
121+
122+
Currently phase architecture is used by 7 rules:
123+
- scala_library
124+
- scala_macro_library
125+
- scala_library_for_plugin_bootstrapping
126+
- scala_binary
127+
- scala_test
128+
- scala_junit_test
129+
- scala_repl
130+
131+
In each of the rule implementation, it calls `run_phases` and returns the information from `phase_final`, which groups the final returns of the rule. To prevent consumers from accidently removing `phase_final` from the list, we make it a non-customizable phase.
132+
133+
To make a new phase, you have to define a new `phase_<PHASE_NAME>.bzl` in `scala/private/phases/`. Function definition should have 2 arguments, `ctx` and `p`. You may expose the information for later phases by returning a `struct`. In some phases, there are multiple phase functions since different rules may take slightly different input arguemnts. You may want to re-expose the phase definition in `scala/private/phases/phases.bzl`, so it's more convenient to access in rule files.
134+
135+
In the rule implementations, put your new phase in `builtin_customizable_phases` list. The phases are executed sequentially, the order matters if the new phase depends on previous phases.
136+
137+
If you are making new return fields of the rule, remember to modify `phase_final`.
138+
139+
### Phase naming convention
140+
Files in `scala/private/phases/`
141+
- `phase_<PHASE_NAME>.bzl`: phase definition file
142+
143+
Function names in `phase_<PHASE_NAME>.bzl`
144+
- `phase_<RULE_NAME>_<PHASE_NAME>`: function with custom inputs of specific rule
145+
- `phase_common_<PHASE_NAME>`: function without custom inputs
146+
- `_phase_default_<PHASE_NAME>`: private function that takes `_args` for custom inputs
147+
- `_phase_<PHASE_NAME>`: private function with the actual logic
148+
149+
See `phase_compile.bzl` for example.

scala/advanced_usage/providers.bzl

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
"""
2+
A phase provider for customizable rules
3+
It is used only when you intend to add functionalities to existing default rules
4+
"""
5+
6+
ScalaRulePhase = provider(
7+
doc = "A custom phase plugin",
8+
fields = {
9+
"custom_phases": "The phases to add. It takes an array of (relation, peer_name, phase_name, phase_function). Please refer to docs/customizable_phase.md for more details.",
10+
},
11+
)

scala/advanced_usage/scala.bzl

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"""
2+
Re-expose the customizable rules
3+
It is used only when you intend to add functionalities to existing default rules
4+
"""
5+
6+
load(
7+
"@io_bazel_rules_scala//scala/private:rules/scala_binary.bzl",
8+
_make_scala_binary = "make_scala_binary",
9+
)
10+
load(
11+
"@io_bazel_rules_scala//scala/private:rules/scala_junit_test.bzl",
12+
_make_scala_junit_test = "make_scala_junit_test",
13+
)
14+
load(
15+
"@io_bazel_rules_scala//scala/private:rules/scala_library.bzl",
16+
_make_scala_library = "make_scala_library",
17+
_make_scala_library_for_plugin_bootstrapping = "make_scala_library_for_plugin_bootstrapping",
18+
_make_scala_macro_library = "make_scala_macro_library",
19+
)
20+
load(
21+
"@io_bazel_rules_scala//scala/private:rules/scala_repl.bzl",
22+
_make_scala_repl = "make_scala_repl",
23+
)
24+
load(
25+
"@io_bazel_rules_scala//scala/private:rules/scala_test.bzl",
26+
_make_scala_test = "make_scala_test",
27+
)
28+
29+
make_scala_binary = _make_scala_binary
30+
make_scala_library = _make_scala_library
31+
make_scala_library_for_plugin_bootstrapping = _make_scala_library_for_plugin_bootstrapping
32+
make_scala_macro_library = _make_scala_macro_library
33+
make_scala_repl = _make_scala_repl
34+
make_scala_junit_test = _make_scala_junit_test
35+
make_scala_test = _make_scala_test

scala/private/phases/api.bzl

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
"""
2+
The phase API for rules implementation
3+
"""
4+
5+
load(
6+
"@io_bazel_rules_scala//scala:advanced_usage/providers.bzl",
7+
_ScalaRulePhase = "ScalaRulePhase",
8+
)
9+
10+
# A method to modify the built-in phase list
11+
# - Insert new phases to the first/last position
12+
# - Insert new phases before/after existing phases
13+
# - Replace existing phases
14+
def _adjust_phases(phases, adjustments):
15+
# Return when no adjustment needed
16+
if len(adjustments) == 0:
17+
return phases
18+
phases = phases[:]
19+
20+
# relation: the position to add a new phase
21+
# peer_name: the existing phase to compare the position with
22+
# phase_name: the name of the new phase, also used to access phase information
23+
# phase_function: the function of the new phase
24+
for (relation, peer_name, phase_name, phase_function) in adjustments:
25+
for idx, (needle, _) in enumerate(phases):
26+
if relation in ["^", "first"]:
27+
phases.insert(0, (phase_name, phase_function))
28+
elif relation in ["$", "last"]:
29+
phases.append((phase_name, phase_function))
30+
elif needle == peer_name:
31+
if relation in ["-", "before"]:
32+
phases.insert(idx, (phase_name, phase_function))
33+
elif relation in ["+", "after"]:
34+
phases.insert(idx + 1, (phase_name, phase_function))
35+
elif relation in ["=", "replace"]:
36+
phases[idx] = (phase_name, phase_function)
37+
return phases
38+
39+
# Execute phases
40+
def run_phases(ctx, builtin_customizable_phases, fixed_phase):
41+
# Loading custom phases
42+
# Phases must be passed in by provider
43+
phase_providers = [
44+
phase_provider[_ScalaRulePhase]
45+
for phase_provider in ctx.attr._phase_providers
46+
if _ScalaRulePhase in phase_provider
47+
]
48+
49+
# Modify the built-in phase list
50+
adjusted_phases = _adjust_phases(
51+
builtin_customizable_phases,
52+
[
53+
phase
54+
for phase_provider in phase_providers
55+
for phase in phase_provider.custom_phases
56+
],
57+
)
58+
59+
# A placeholder for data shared with later phases
60+
global_provider = {}
61+
current_provider = struct(**global_provider)
62+
for (name, function) in adjusted_phases + [fixed_phase]:
63+
# Run a phase
64+
new_provider = function(ctx, current_provider)
65+
66+
# If a phase returns data, append it to global_provider
67+
# for later phases to access
68+
if new_provider != None:
69+
global_provider[name] = new_provider
70+
current_provider = struct(**global_provider)
71+
72+
# The final return of rules implementation
73+
return current_provider
74+
75+
# A method to pass in phase provider
76+
def extras_phases(extras):
77+
return {
78+
"_phase_providers": attr.label_list(
79+
default = [
80+
phase_provider
81+
for extra in extras
82+
for phase_provider in extra["phase_providers"]
83+
],
84+
providers = [_ScalaRulePhase],
85+
),
86+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#
2+
# PHASE: collect exports jars
3+
#
4+
# DOCUMENT THIS
5+
#
6+
load(
7+
"@io_bazel_rules_scala//scala/private:common.bzl",
8+
"collect_jars",
9+
)
10+
11+
def phase_collect_exports_jars(ctx, p):
12+
# Add information from exports (is key that AFTER all build actions/runfiles analysis)
13+
# Since after, will not show up in deploy_jar or old jars runfiles
14+
# Notice that compile_jars is intentionally transitive for exports
15+
return collect_jars(ctx.attr.exports)

0 commit comments

Comments
 (0)