Skip to content

Commit f7af4af

Browse files
author
Jaden Peterson
committed
Include macros' transitive runtime dependencies on the compile classpath
1 parent e2e0170 commit f7af4af

File tree

6 files changed

+95
-2
lines changed

6 files changed

+95
-2
lines changed

scala/private/common.bzl

+27-2
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

test/macros/BUILD

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
load("//scala:scala.bzl", "scala_library", "scala_macro_library")
2+
3+
scala_library(
4+
name = "macro-dependency",
5+
srcs = ["MacroDependency.scala"],
6+
)
7+
8+
scala_macro_library(
9+
name = "macro-with-dependencies",
10+
srcs = ["MacroWithDependencies.scala"],
11+
deps = [":macro-dependency"],
12+
)
13+
14+
scala_library(
15+
name = "macro-with-dependencies-user",
16+
srcs = ["MacroWithDependenciesUser.scala"],
17+
# Without this, `:macro-dependency` will be flagged as an unused dependency. But we want to test that despite it
18+
# appearing before `:macro-with-dependencies` in `deps`, its runtime JAR is included before its compile JAR in the
19+
# compile classpath
20+
unused_dependency_checker_mode = "off",
21+
deps = [
22+
":macro-dependency",
23+
":macro-with-dependencies",
24+
],
25+
)

test/macros/MacroDependency.scala

+5
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+
}
+18
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+
}
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

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# shellcheck source=./test_runner.sh
2+
dir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
3+
. "${dir}"/test_runner.sh
4+
. "${dir}"/test_helper.sh
5+
runner=$(get_test_runner "${1:-local}")
6+
7+
macros_can_have_dependencies() {
8+
bazel build //test/macros:macro-with-dependencies-user
9+
}
10+
11+
$runner macros_can_have_dependencies

0 commit comments

Comments
 (0)