Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 48 additions & 1 deletion unstable/multiscala/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,61 @@ scala_binary(
```
would act with the current default single version configuration mostly exactly as it does today: it would create a single jar for each target, e.g., `lib.jar` and `app.jar` (but we would probably add some aliases, too, with version suffixes as commmonly seen in maven).

A change of configuration to specify two scala versions would not require any changes from the user. In other words, the default behavior would be to build everything against all versions and use arguments to reduce targets, not require them to increase them.
It would be highly desriable if a a change of configuration to specify two scala versions would not require any changes from the user. In other words, the default behavior would be to build everything against all versions and use arguments to reduce targets, not require them to increase them. At this point, that may not be feasible though close approximation probably are. See below

So in the example above, with two scala versions configured, a build would create, for example, `lib_2_11.jar`, `lib_2_12.jar`, `app_2_11.jar` and `app_2_12.jar`. The mutliscala code will create the versioned targets based on the version deps. (This is a simplified example. As mentioned, I think we'll end up creating one jar for every version and then a set of aliases to give people the names that they expect.)

To do this, we'd need to change `scala_library` from a rule to a macro. The macro has access to the configuration (which is why it's an external repo) and can instantiate the necessary targets and aliases.

I do wonder if folks will consider this _too magic_. I can say that the developers I work with would prefer this to manual copying or having to write a starlark loop themselves for every target.

## Challenges to supporting multiscala without build file changes

The primary challenge here is `deps` and `runtime_deps` (and anything else of that ilk). In the example above, version-specific jars of `app` need to depend on the version-specific jars of `lib`. At this point, I don't see any way to do this with a `scala_binary` macro: information about the depended-on target isn't available during loading and it's too late to make dependence changes at analysis.

So far, the only thing I've come up with is to add `scala_deps` and `scala_runtime_deps` arguments to the `scala_*` macros. The macros can then add the necessary version information to the labels before combining them with the standard, unaltered sets (`deps`, `runtime_deps`) and then running the standard rule to create a target. So to migrate, a user would have to move existing dependencies that need automatic scala version naming to `scala_deps`, leaving non-versioned (java) deps in `deps`. It would be easy enough to add compatibility no-op shims to allow people to future proof while still sticking with stable code though correctness would not be checked.

## Using defaults and aliases

When declaring something like the library `lib` above, one can imagine many ways `lib` will be named. In the uniscala mode, this would simply be `lib.jar`. Using the standard scala pattern, one would expect something like `lib_2_12.jar` and `lib_2_11.jar`.

If we support concurrent minor builds, we can imagine
```
lib_2_11_10.jar
lib_2_11_12.jar
lib_2_12_10.jar
lib_2_13_1.jar
```
This could break build environments that ran the multiscala build (for test purposes) but still expected a uniscala-like target, i.e., `lib.jar`.

The proposed model is that users can optionally configure/ask for defaults. Based on that default, aliases are created that remove version information from names. This could be both a global default but also could be done for each major version.

So in the example above if we configured a 2.11 default of 12, a 2.12 default of 10, and a global default of 2.12, we would expect
```
lib_2_11_10.jar
lib_2_11_12.jar
lib_2_11.jar
lib_2_12_10.jar
lib_2_12.jar
lib_2_13_1.jar
lib.jar
```
We might want a `lib_2.jar` as well, just to be complete.

Defaults are explicit unless there's only one option, e.g., 2.12.10, above in which case they're implicit. They can be explicitly inhibited with a value of None.

One thing I noted and I'm not sure about is that AFAICT, bazel implements aliases with copies. That means keeping unneeded aliases takes disk space. Not sure if this is an issue ...

I suspect, instead, what we might want to do is provide a macro argument that indicates we want a specifc version to be the default, e.g., something like
```
scala_binary(
name = "my-app",
...
default_version = "2.12",
)
```
The macro would alias this and only this version of this target. This would produce far less copies at the expense of requiring users to add this to all rules where they want this behavior. Alternatively, we could have a configuration along the lines of `alias_default_binary = True`. That might be the best trade-off: not aliasing every library target but not requiring the user annotate every binary target. Supporting both would be fairly simple.

## External Repos

See [External Repositories](ExternalReposistories.md)
2 changes: 1 addition & 1 deletion unstable/multiscala/configuration.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ _repo = repository_rule(
attrs = {
"starlark_string": attr.string(mandatory = True),
"_template": attr.label(
default = ":configuration.bzl.tpl",
default = ":private/templates/configuration.bzl.tpl",
),
},
)
Expand Down
24 changes: 9 additions & 15 deletions unstable/multiscala/multiscala.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,12 @@
TBD
"""

load(
"//scala:scala.bzl",
_scala_binary = "scala_binary",
_scala_library = "scala_library",
_scala_test = "scala_test",
)

def scala_binary(**kwargs):
_scala_binary(**kwargs)

def scala_library(**kwargs):
_scala_library(**kwargs)

def scala_test(**kwargs):
_scala_test(**kwargs)
load(":configuration.bzl", _toolchain_label = "toolchain_label")
load(":private/macros/scala_binary.bzl", _scala_binary = "scala_binary")
load(":private/macros/scala_library.bzl", _scala_library = "scala_library")
load(":private/macros/scala_test.bzl", _scala_test = "scala_test")

scala_library = _scala_library
scala_binary = _scala_binary
scala_test = _scala_test
toolchain_label = _toolchain_label
4 changes: 3 additions & 1 deletion unstable/multiscala/private/example/App.scala
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
object App extends scala.App {
println(s"hello, world from ${scala.util.Properties.versionString}!")
def version = scala.util.Properties.versionString

println(s"hello, world from $version!")
}
3 changes: 2 additions & 1 deletion unstable/multiscala/private/example/AppTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import org.scalatest.matchers.should.Matchers
class AppTest extends AnyFlatSpec with Matchers {
it should "have a successful test" in {
System.err.println(s"hello, world from ${scala.util.Properties.versionString}!")
true should be (true)

App.version should be (scala.util.Properties.versionString)
}
}
60 changes: 44 additions & 16 deletions unstable/multiscala/private/example/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -1,45 +1,73 @@
load(
"@io_bazel_rules_scala//unstable/multiscala:configuration.bzl",
"toolchain_label",
)
load(
"@io_bazel_rules_scala//unstable/multiscala:multiscala.bzl",
_scala_binary = "scala_binary",
_scala_library = "scala_library",
_scala_test = "scala_test",
"scala_binary",
"scala_library",
"scala_test",
"toolchain_label",
)

_scala_library(
# default case: builds all configured versions with version suffixes

scala_library(
name = "library",
srcs = ["App.scala"],
srcs = glob(
["*.scala"],
exclude = ["*Test.scala"],
),
)

_scala_binary(
scala_binary(
name = "app",
main_class = "App",
runtime_deps = [":library"],
scala_runtime_deps = [":library"],
)

_scala_test(
scala_test(
name = "test",
srcs = ["AppTest.scala"],
scala_deps = [":library"],
)

_scala_library(
# explict version configuration

scala_library(
name = "library_with_explict_version",
srcs = ["App.scala"],
scala = "2.12",
)

scala_binary(
name = "app_with_explict_version",
main_class = "App",
scala = "2.12",
scala_runtime_deps = [":library_with_explict_version"],
)

scala_test(
name = "test_with_explict_version",
srcs = ["AppTest.scala"],
scala = "2.12",
scala_deps = [":library_with_explict_version"],
)

# explict toolchain configuration: this disabled multiscala: you're on your own ...

scala_library(
name = "library_with_explict_toolchain",
srcs = ["App.scala"],
toolchains = [toolchain_label("scala", "2.11")],
)

_scala_binary(
scala_binary(
name = "app_with_explict_toolchain",
main_class = "App",
toolchains = [toolchain_label("scala", "2.11")],
runtime_deps = [":library"],
runtime_deps = [":library_with_explict_toolchain"],
)

_scala_test(
scala_test(
name = "test_with_explict_toolchain",
srcs = ["AppTest.scala"],
toolchains = [toolchain_label("scala", "2.11")],
deps = [":library_with_explict_toolchain"],
)
61 changes: 61 additions & 0 deletions unstable/multiscala/private/macros/scala_binary.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""multiscala equivalents to scala/scala.bzl

TBD
"""

load("@bazel_skylib//lib:dicts.bzl", _dicts = "dicts")
load(
"//scala:scala.bzl",
_scala_binary_rule = "scala_binary",
)
load(
"//unstable/multiscala:configuration.bzl",
_toolchain_label = "toolchain_label",
)
load(
"//unstable/multiscala:private/macros/tools.bzl",
_combine_kwargs = "combine_kwargs",
_remove_toolchains = "remove_toolchains",
_target_versions = "target_versions",
)

_binary_suffixes = ["", "_deploy.jar"]

def _create_scala_binary(version, **kwargs):
kwargs = _remove_toolchains(kwargs, version)
kwargs = _combine_kwargs(kwargs, version["mvn"])
kwargs.update(
toolchains = [_toolchain_label("scala", version["mvn"])],
)

# print(kwargs)
_scala_binary_rule(**kwargs)

def scala_binary(
scala_deps = [],
scala_runtime_deps = [],
deps = [],
runtime_deps = [],
scala = None,
**kwargs):
"""create a multi-scala binary

Args:
scala_deps: deps that require scala version naming
scala_runtime_deps: deps that require scala version naming
deps: deps that do not require scala version changes
runtime_deps: runtime_deps that do not require scala version changes
scala: verisons of scala to build for
**kwargs: standard scala_binary arguments
"""
kwargs = _dicts.add(kwargs)
kwargs.update(
scala = scala,
deps = deps,
runtime_deps = runtime_deps,
scala_deps = scala_deps,
scala_runtime_deps = scala_runtime_deps,
)

for version in _target_versions(kwargs):
_create_scala_binary(version, **kwargs)
57 changes: 57 additions & 0 deletions unstable/multiscala/private/macros/scala_library.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""multiscala equivalents to scala/scala.bzl

TBD
"""

load("@bazel_skylib//lib:dicts.bzl", _dicts = "dicts")
load(
"//scala:scala.bzl",
_scala_library_rule = "scala_library",
)
load(
"//unstable/multiscala:configuration.bzl",
_toolchain_label = "toolchain_label",
)
load(
"//unstable/multiscala:private/macros/tools.bzl",
_combine_kwargs = "combine_kwargs",
_remove_toolchains = "remove_toolchains",
_target_versions = "target_versions",
)

def _create_scala_library(version, **kwargs):
kwargs = _remove_toolchains(kwargs, version)
kwargs = _combine_kwargs(kwargs, version["mvn"])
_scala_library_rule(
toolchains = [_toolchain_label("scala", version["mvn"])],
**kwargs
)

def scala_library(
scala_deps = [],
scala_runtime_deps = [],
deps = [],
runtime_deps = [],
scala = None,
**kwargs):
"""create a multi-scala library

Args:
scala_deps: deps that require scala version naming
scala_runtime_deps: deps that require scala version naming
deps: deps that do not require scala version changes
runtime_deps: runtime_deps that do not require scala version changes
scala: verisons of scala to build for
**kwargs: standard scala_library arguments
"""
kwargs = _dicts.add(kwargs)
kwargs.update(
scala = scala,
deps = deps,
runtime_deps = runtime_deps,
scala_deps = scala_deps,
scala_runtime_deps = scala_runtime_deps,
)

for version in _target_versions(kwargs):
_create_scala_library(version, **kwargs)
56 changes: 56 additions & 0 deletions unstable/multiscala/private/macros/scala_test.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""multiscala equivalents to scala/scala.bzl

TBD
"""

load("@bazel_skylib//lib:dicts.bzl", _dicts = "dicts")
load(
"//scala:scala.bzl",
_scala_test_rule = "scala_test",
)
load(
"//unstable/multiscala:configuration.bzl",
_toolchain_label = "toolchain_label",
)
load(
"//unstable/multiscala:private/macros/tools.bzl",
_combine_kwargs = "combine_kwargs",
_remove_toolchains = "remove_toolchains",
_target_versions = "target_versions",
)

def _create_scala_test(version, **kwargs):
kwargs = _remove_toolchains(kwargs, version)
kwargs["toolchains"] = [_toolchain_label("scala", version["mvn"])]
kwargs = _combine_kwargs(kwargs, version["mvn"])
_scala_test_rule(**kwargs)

def scala_test(
scala_deps = [],
scala_runtime_deps = [],
deps = [],
runtime_deps = [],
scala = None,
**kwargs):
"""create a multi-scala test

Args:
scala_deps: deps that require scala version naming
scala_runtime_deps: deps that require scala version naming
deps: deps that do not require scala version changes
runtime_deps: runtime_deps that do not require scala version changes
scala: verisons of scala to build for
**kwargs: standard scala_test arguments
"""

kwargs = _dicts.add(kwargs)
kwargs.update(
scala = scala,
deps = deps,
runtime_deps = runtime_deps,
scala_deps = scala_deps,
scala_runtime_deps = scala_runtime_deps,
)

for version in _target_versions(kwargs):
_create_scala_test(version, **kwargs)
Loading