Skip to content

Commit 905cf96

Browse files
author
Jaden Peterson
committed
Include macros' transitive runtime dependencies on the compile classpath
1 parent 5188305 commit 905cf96

File tree

7 files changed

+103
-6
lines changed

7 files changed

+103
-6
lines changed

scala/private/common.bzl

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
load("@io_bazel_rules_scala//scala:jars_to_labels.bzl", "JarsToLabelsInfo")
22
load("@io_bazel_rules_scala//scala:plusone.bzl", "PlusOneDeps")
3+
load("@io_bazel_rules_scala//scala:providers.bzl", "ScalaInfo")
34
load("@bazel_skylib//lib:paths.bzl", "paths")
45

56
def write_manifest_file(actions, output_file, main_class):
@@ -22,6 +23,7 @@ def collect_jars(
2223
compile_jars = []
2324
runtime_jars = []
2425
deps_providers = []
26+
macro_classpath = []
2527

2628
for dep_target in dep_targets:
2729
# we require a JavaInfo for dependencies
@@ -50,11 +52,34 @@ def collect_jars(
5052
java_provider.compile_jars.to_list(),
5153
)
5254

55+
# Macros are different from ordinary targets in that they’re used at compile time instead of at runtime. That
56+
# means that both their compile-time classpath and runtime classpath are needed at compile time. We could have
57+
# `scala_macro_library` targets include their runtime dependencies in their compile-time dependencies, but then
58+
# we wouldn't have any guarantees classpath order.
59+
#
60+
# Consider the following scenario. Target A depends on targets B and C. Target C is a macro target, whereas
61+
# target B isn't. Targets C depends on target B. If target A doesn't include the runtime version of target C on
62+
# the compile classpath before the compile (`ijar`d) version of target B that target C depends on, then target A
63+
# won't use the correct version of target B at compile-time when evaluating the macros contained in target C.
64+
#
65+
# For that reason, we opt for a different approach: have `scala_macro_library` targets export `JavaInfo`
66+
# providers as normal, but put their transitive runtime dependencies first on the classpath. Note that we
67+
# shouldn't encounter any issues with external dependencies, so long as they aren't `ijar`d.
68+
if ScalaInfo in dep_target and dep_target[ScalaInfo].contains_macros:
69+
macro_classpath.append(java_provider.transitive_runtime_jars)
70+
71+
add_labels_of_jars_to(
72+
jars2labels,
73+
dep_target,
74+
[],
75+
java_provider.transitive_runtime_jars.to_list(),
76+
)
77+
5378
return struct(
54-
compile_jars = depset(transitive = compile_jars),
79+
compile_jars = depset(order = "preorder", transitive = macro_classpath + compile_jars),
5580
transitive_runtime_jars = depset(transitive = runtime_jars),
5681
jars2labels = JarsToLabelsInfo(jars_to_labels = jars2labels),
57-
transitive_compile_jars = depset(transitive = transitive_compile_jars),
82+
transitive_compile_jars = depset(order = "preorder", transitive = macro_classpath + transitive_compile_jars),
5883
deps_providers = deps_providers,
5984
)
6085

scala/private/rules/scala_doc.bzl

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
"""Scaladoc support"""
22

3+
load("@io_bazel_rules_scala//scala:providers.bzl", "ScalaInfo")
34
load("@io_bazel_rules_scala//scala/private:common.bzl", "collect_plugin_paths")
45

56
ScaladocAspectInfo = provider(fields = [
67
"src_files", #depset[File]
78
"compile_jars", #depset[File]
9+
"macro_classpath", #depset[File]
810
"plugins", #depset[Target]
911
])
1012

@@ -29,23 +31,28 @@ def _scaladoc_aspect_impl(target, ctx, transitive = True):
2931
if hasattr(ctx.rule.attr, "plugins"):
3032
plugins = depset(ctx.rule.attr.plugins)
3133

34+
macro_classpath = []
35+
36+
for dependency in ctx.rule.attr.deps:
37+
if ScalaInfo in dependency and dependency[ScalaInfo].contains_macros:
38+
macro_classpath.append(dependency[JavaInfo].transitive_runtime_jars)
39+
3240
# Sometimes we only want to generate scaladocs for a single target and not all of its
3341
# dependencies
3442
transitive_srcs = depset()
35-
transitive_compile_jars = depset()
3643
transitive_plugins = depset()
3744

3845
if transitive:
3946
for dep in ctx.rule.attr.deps:
4047
if ScaladocAspectInfo in dep:
4148
aspec_info = dep[ScaladocAspectInfo]
4249
transitive_srcs = aspec_info.src_files
43-
transitive_compile_jars = aspec_info.compile_jars
4450
transitive_plugins = aspec_info.plugins
4551

4652
return [ScaladocAspectInfo(
4753
src_files = depset(transitive = [src_files, transitive_srcs]),
48-
compile_jars = depset(transitive = [compile_jars, transitive_compile_jars]),
54+
compile_jars = depset(transitive = [compile_jars]),
55+
macro_classpath = depset(transitive = macro_classpath),
4956
plugins = depset(transitive = [plugins, transitive_plugins]),
5057
)]
5158

@@ -73,11 +80,15 @@ def _scala_doc_impl(ctx):
7380
src_files = depset(transitive = [dep[ScaladocAspectInfo].src_files for dep in ctx.attr.deps])
7481
compile_jars = depset(transitive = [dep[ScaladocAspectInfo].compile_jars for dep in ctx.attr.deps])
7582

83+
# See the documentation for `collect_jars` in `scala/private/common.bzl` to understand why this is prepended to the
84+
# classpath
85+
macro_classpath = depset(transitive = [dep[ScaladocAspectInfo].macro_classpath for dep in ctx.attr.deps])
86+
7687
# Get the 'real' paths to the plugin jars.
7788
plugins = collect_plugin_paths(depset(transitive = [dep[ScaladocAspectInfo].plugins for dep in ctx.attr.deps]).to_list())
7889

7990
# Construct the full classpath depset since we need to add compiler plugins too.
80-
classpath = depset(transitive = [plugins, compile_jars])
91+
classpath = depset(transitive = [macro_classpath, plugins, compile_jars])
8192

8293
# Construct scaladoc args, which also include scalac args.
8394
# See `scaladoc -help` for more information.

test/macros/BUILD

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,27 @@ scala_library(
2222
srcs = ["MacroUser.scala"],
2323
deps = [":correct-macro"],
2424
)
25+
26+
scala_library(
27+
name = "macro-dependency",
28+
srcs = ["MacroDependency.scala"],
29+
)
30+
31+
scala_macro_library(
32+
name = "macro-with-dependencies",
33+
srcs = ["MacroWithDependencies.scala"],
34+
deps = [":macro-dependency"],
35+
)
36+
37+
scala_library(
38+
name = "macro-with-dependencies-user",
39+
srcs = ["MacroWithDependenciesUser.scala"],
40+
# Without this, `:macro-dependency` will be flagged as an unused dependency. But we want to test that despite it
41+
# appearing before `:macro-with-dependencies` in `deps`, its runtime JAR is included before its compile JAR in the
42+
# compile classpath
43+
unused_dependency_checker_mode = "off",
44+
deps = [
45+
":macro-dependency",
46+
":macro-with-dependencies",
47+
],
48+
)

test/macros/MacroDependency.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package macros
2+
3+
object MacroDependency {
4+
def isEven(number: Int): Boolean = number % 2 == 0
5+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package macros
2+
3+
import scala.language.experimental.macros
4+
import scala.reflect.macros.blackbox
5+
6+
object MacroWithDependencies {
7+
def isEvenMacro(number: Int): Boolean = macro isEvenMacroImpl
8+
def isEvenMacroImpl(context: blackbox.Context)(number: context.Expr[Int]): context.Expr[Boolean] = {
9+
import context.universe._
10+
11+
val value = number.tree match {
12+
case Literal(Constant(value: Int)) => value
13+
case _ => throw new Exception(s"Expected ${number.tree} to be a literal.")
14+
}
15+
16+
context.Expr(Literal(Constant(MacroDependency.isEven(value))))
17+
}
18+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package macros
2+
3+
object MacroWithDependenciesUser {
4+
def main(arguments: Array[String]): Unit = {
5+
println(s"0 is even via macro: ${MacroWithDependencies.isEvenMacro(0)}")
6+
println(s"1 is even via macro: ${MacroWithDependencies.isEvenMacro(1)}")
7+
println(s"1 + 1 is even macro: ${MacroWithDependencies.isEvenMacro(1 + 1)}")
8+
}
9+
}

test/shell/test_macros.sh

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,10 @@ correct_macro_user_builds() {
1313
bazel build //test/macros:correct-macro-user
1414
}
1515

16+
macros_can_have_dependencies() {
17+
bazel build //test/macros:macro-with-dependencies-user
18+
}
19+
1620
$runner incorrect_macro_user_does_not_build
1721
$runner correct_macro_user_builds
22+
$runner macros_can_have_dependencies

0 commit comments

Comments
 (0)