diff --git a/examples/testing/multi_frameworks_toolchain/BUILD b/examples/testing/multi_frameworks_toolchain/BUILD
index 27eedcf7e..53baf7211 100644
--- a/examples/testing/multi_frameworks_toolchain/BUILD
+++ b/examples/testing/multi_frameworks_toolchain/BUILD
@@ -31,5 +31,7 @@ setup_scala_testing_toolchain(
],
specs2_junit_classpath = [
"@io_bazel_rules_scala_org_specs2_specs2_junit",
+ "@org_portable_scala_portable_scala_reflect",
+ "@org_scala_sbt_test_interface",
],
)
diff --git a/scala/BUILD b/scala/BUILD
index 7663b980a..94f81a12b 100644
--- a/scala/BUILD
+++ b/scala/BUILD
@@ -34,6 +34,7 @@ scala_toolchain(
[
toolchain(
name = tc,
+ target_settings = ["@io_bazel_rules_scala_config//:scala_version" + version_suffix(SCALA_VERSION)],
toolchain = tc + "_impl",
toolchain_type = "//scala:toolchain_type",
visibility = ["//visibility:public"],
diff --git a/scala/private/common_attributes.bzl b/scala/private/common_attributes.bzl
index a71080296..a33c396ed 100644
--- a/scala/private/common_attributes.bzl
+++ b/scala/private/common_attributes.bzl
@@ -78,6 +78,11 @@ common_attrs.update({
executable = True,
cfg = "exec",
),
+ "_dottyijar": attr.label(
+ cfg = "exec",
+ default = "//src/scala/io/bazel/rules_scala/dottyijar",
+ executable = True,
+ ),
})
implicit_deps = {
diff --git a/scala/private/macros/scala_repositories.bzl b/scala/private/macros/scala_repositories.bzl
index 81b6c9cbf..f7f462510 100644
--- a/scala/private/macros/scala_repositories.bzl
+++ b/scala/private/macros/scala_repositories.bzl
@@ -37,6 +37,13 @@ _COMPILER_SOURCES_ENTRY_TEMPLATE = """
"@io_bazel_rules_scala_config//:scala_version{scala_version_suffix}":
"@scala_compiler_source{scala_version_suffix}//:src","""
+_IZUMI_REFLECT_DEPS = ["dev_zio_izumi_reflect_thirdparty_boopickle_shaded"]
+_JUNIT_DEPS = ["io_bazel_rules_scala_org_hamcrest_hamcrest_core"]
+_SPECS2_DEPS = [
+ "org_portable_scala_portable_scala_reflect",
+ "org_scala_sbt_test_interface",
+]
+
def _compiler_sources_repo_impl(rctx):
sources = [
_COMPILER_SOURCES_ENTRY_TEMPLATE.format(
@@ -144,12 +151,19 @@ def rules_scala_setup(scala_compiler_srcjar = None):
def _artifact_ids(scala_version):
result = [
+ "commons_io_commons_io",
+ "io_bazel_rules_scala_junit_junit",
+ "io_bazel_rules_scala_org_specs2_specs2_common",
+ "io_bazel_rules_scala_org_specs2_specs2_core",
+ "io_bazel_rules_scala_org_specs2_specs2_fp",
+ "io_bazel_rules_scala_org_specs2_specs2_junit",
+ "io_bazel_rules_scala_org_specs2_specs2_matcher",
"io_bazel_rules_scala_scala_compiler",
"io_bazel_rules_scala_scala_library",
"io_bazel_rules_scala_scala_parser_combinators",
"io_bazel_rules_scala_scala_xml",
"org_scala_lang_modules_scala_collection_compat",
- ]
+ ] + _JUNIT_DEPS + _SPECS2_DEPS
if scala_version.startswith("2."):
result.extend([
@@ -167,12 +181,14 @@ def _artifact_ids(scala_version):
if scala_version.startswith("3."):
result.extend([
+ "dev_zio_izumi_reflect",
"io_bazel_rules_scala_scala_asm",
"io_bazel_rules_scala_scala_compiler_2",
"io_bazel_rules_scala_scala_interfaces",
"io_bazel_rules_scala_scala_library_2",
"io_bazel_rules_scala_scala_reflect_2",
"io_bazel_rules_scala_scala_tasty_core",
+ "io_bazel_rules_scala_scala_tasty_inspector",
"org_jline_jline_native",
"org_jline_jline_reader",
"org_jline_jline_terminal",
@@ -181,6 +197,8 @@ def _artifact_ids(scala_version):
"org_scala_sbt_util_interface",
])
+ result.extend(_IZUMI_REFLECT_DEPS)
+
return result
def rules_scala_toolchain_deps_repositories(
diff --git a/scala/private/phases/phase_compile.bzl b/scala/private/phases/phase_compile.bzl
index d1534aabb..c1df0ada9 100644
--- a/scala/private/phases/phase_compile.bzl
+++ b/scala/private/phases/phase_compile.bzl
@@ -41,8 +41,10 @@ def phase_compile_library(ctx, p):
return _phase_compile_default(ctx, p, args)
def phase_compile_library_for_plugin_bootstrapping(ctx, p):
+ is_scala_2 = ctx.toolchains["@io_bazel_rules_scala//scala:toolchain_type"].scala_version.startswith("2.")
+
args = struct(
- buildijar = ctx.attr.build_ijar,
+ buildijar = ctx.attr.build_ijar and is_scala_2,
)
return _phase_compile_default(ctx, p, args)
@@ -102,13 +104,11 @@ def phase_compile_common(ctx, p):
return _phase_compile_default(ctx, p)
def _phase_compile_default(ctx, p, _args = struct()):
- buildijar_default_value = True if ctx.toolchains["@io_bazel_rules_scala//scala:toolchain_type"].scala_version.startswith("2.") else False
-
return _phase_compile(
ctx,
p,
_args.srcjars if hasattr(_args, "srcjars") else depset(),
- _args.buildijar if hasattr(_args, "buildijar") else buildijar_default_value,
+ not hasattr(_args, "buildijar") or _args.buildijar,
_args.implicit_junit_deps_needed_for_java_compilation if hasattr(_args, "implicit_junit_deps_needed_for_java_compilation") else [],
unused_dependency_checker_ignored_targets = _args.unused_dependency_checker_ignored_targets if hasattr(_args, "unused_dependency_checker_ignored_targets") else [],
)
@@ -228,12 +228,7 @@ def _compile_or_empty(
# build ijar if needed
if buildijar:
- ijar = java_common.run_ijar(
- ctx.actions,
- jar = ctx.outputs.jar,
- target_label = ctx.label,
- java_toolchain = specified_java_compile_toolchain(ctx),
- )
+ ijar = _build_ijar(ctx)
else:
# macro code needs to be available at compile-time,
# so set ijar == jar
@@ -266,6 +261,41 @@ def _compile_or_empty(
merged_provider = merged_provider,
)
+def _build_ijar(ctx):
+ scala_version = ctx.toolchains["@io_bazel_rules_scala//scala:toolchain_type"].scala_version
+ is_scala_2 = scala_version.startswith("2.")
+
+ if is_scala_2:
+ return java_common.run_ijar(
+ ctx.actions,
+ jar = ctx.outputs.jar,
+ target_label = ctx.label,
+ java_toolchain = specified_java_compile_toolchain(ctx),
+ )
+
+ is_scala_3_3_or_lower = scala_version.startswith("3.") and int(scala_version.split(".")[1]) < 4
+
+ # Prior to Scala v3.4.0, TASTy files couldn't be read directly without a `.class` file present and its
+ # "TASTY" attributes preserved:
+ # https://github.com/scala/scala3/pull/17594
+ if is_scala_3_3_or_lower:
+ return ctx.outputs.jar
+
+ output = ctx.actions.declare_file("{}-ijar.jar".format(ctx.label.name))
+ arguments = ctx.actions.args()
+ arguments.add(ctx.outputs.jar)
+ arguments.add(output)
+
+ ctx.actions.run(
+ arguments = [arguments],
+ executable = ctx.executable._dottyijar,
+ inputs = [ctx.outputs.jar],
+ mnemonic = "DottyIjar",
+ outputs = [output],
+ )
+
+ return output
+
def _build_nosrc_jar(ctx):
resources = [s + ":" + t for t, s in _resource_paths(ctx.files.resources, ctx.attr.resource_strip_prefix)]
diff --git a/scala_config.bzl b/scala_config.bzl
index 70cf132c3..d8ffeaf4c 100644
--- a/scala_config.bzl
+++ b/scala_config.bzl
@@ -29,12 +29,24 @@ def _store_config(repository_ctx):
)
# All versions supported
- scala_versions = repository_ctx.attr.scala_versions
+ if "SCALA_VERSIONS" in repository_ctx.os.environ:
+ scala_versions = repository_ctx.os.environ["SCALA_VERSIONS"].split(",")
+ else:
+ scala_versions = repository_ctx.attr.scala_versions
+
if not scala_versions:
scala_versions = [scala_version]
elif scala_version not in scala_versions:
fail("You have to include the default Scala version (%s) in the `scala_versions` list." % scala_version)
+ # dottyijar requires Scala v3.6.2, but we don't want to force the caller to always provide 3.6.2. Therefore, we
+ # append it if it hasn't been provided.
+ #
+ # Once we move to Bzlmod, this shouldn't be a problem, since we can register a toolchain for Scala v3.6.2 without
+ # requiring users of this ruleset to do so.
+ if "3.6.2" not in scala_versions:
+ scala_versions = scala_versions + ["3.6.2"]
+
enable_compiler_dependency_tracking = repository_ctx.os.environ.get(
"ENABLE_COMPILER_DEPENDENCY_TRACKING",
str(repository_ctx.attr.enable_compiler_dependency_tracking),
diff --git a/scripts/create_repository.py b/scripts/create_repository.py
index 9ade78254..8931096c1 100755
--- a/scripts/create_repository.py
+++ b/scripts/create_repository.py
@@ -76,12 +76,12 @@ def select_root_artifacts(scala_version, scala_major, is_scala_3) -> List[str]:
scala_2_version = scala_version
scala_2_major = scala_major
- scalatest_major = scala_major
+ scala_3_major = scala_major
if is_scala_3:
scala_2_version = max_scala_2_version
scala_2_major = max_scala_2_major
- scalatest_major = '3'
+ scala_3_major = '3'
scalafmt_version = SCALAFMT_VERSION
scalapb_version = SCALAPB_VERSION
@@ -92,29 +92,37 @@ def select_root_artifacts(scala_version, scala_major, is_scala_3) -> List[str]:
scalapb_version = '0.9.8'
protoc_bridge_version = '0.7.14'
+ if is_scala_3:
+ # Versions greater than v4.20.0 depend on a version of the Scala standard library greater than v3.1.3. This is a
+ # problem because Scala v3.1.3, which we support, needs to use a matching version of the Scala standard library.
+ specs2_version = '4.20.0'
+ elif scala_major == '2.11':
+ specs2_version = '4.10.6'
+ else:
+ specs2_version = '4.20.9'
+
root_artifacts = [
- 'com.google.api.grpc:proto-google-common-protos:' +
- GRPC_COMMON_PROTOS_VERSION,
+ f'com.google.api.grpc:proto-google-common-protos:{GRPC_COMMON_PROTOS_VERSION}',
f'com.google.guava:guava:{GUAVA_VERSION}',
f'com.google.protobuf:protobuf-java:{PROTOBUF_JAVA_VERSION}',
- f'com.thesamet.scalapb:compilerplugin_{scala_2_major}:' +
- scalapb_version,
- f'com.thesamet.scalapb:protoc-bridge_{scala_2_major}:' +
- protoc_bridge_version,
- f'com.thesamet.scalapb:scalapb-runtime_{scala_2_major}:' +
- scalapb_version,
- f'com.thesamet.scalapb:scalapb-runtime-grpc_{scala_2_major}:' +
- scalapb_version,
- f'org.scala-lang.modules:scala-parser-combinators_{scala_2_major}:' +
- PARSER_COMBINATORS_VERSION,
+ f'com.thesamet.scalapb:compilerplugin_{scala_2_major}:{scalapb_version}',
+ f'com.thesamet.scalapb:protoc-bridge_{scala_2_major}:{protoc_bridge_version}',
+ f'com.thesamet.scalapb:scalapb-runtime-grpc_{scala_2_major}:{scalapb_version}',
+ f'com.thesamet.scalapb:scalapb-runtime_{scala_2_major}:{scalapb_version}',
+ 'commons-io:commons-io:2.18.0',
+ f'org.scala-lang.modules:scala-parser-combinators_{scala_2_major}:{PARSER_COMBINATORS_VERSION}',
f'org.scala-lang:scala-compiler:{scala_2_version}',
f'org.scala-lang:scala-library:{scala_2_version}',
f'org.scala-lang:scala-reflect:{scala_2_version}',
f'org.scala-lang:scalap:{scala_2_version}',
f'org.scalameta:scalafmt-core_{scala_2_major}:{scalafmt_version}',
- f'org.scalatest:scalatest_{scalatest_major}:{SCALATEST_VERSION}',
- f'org.typelevel:kind-projector_{scala_2_version}:' +
- KIND_PROJECTOR_VERSION,
+ f'org.scalatest:scalatest_{scala_3_major}:{SCALATEST_VERSION}',
+ f'org.specs2:specs2-common_{scala_3_major}:{specs2_version}',
+ f'org.specs2:specs2-core_{scala_3_major}:{specs2_version}',
+ f'org.specs2:specs2-fp_{scala_3_major}:{specs2_version}',
+ f'org.specs2:specs2-junit_{scala_3_major}:{specs2_version}',
+ f'org.specs2:specs2-matcher_{scala_3_major}:{specs2_version}',
+ f'org.typelevel:kind-projector_{scala_2_version}:{KIND_PROJECTOR_VERSION}',
] + [f'io.grpc:grpc-{lib}:{GRPC_VERSION}' for lib in GRPC_LIBS]
if scala_version == max_scala_2_version or is_scala_3:
@@ -123,16 +131,20 @@ def select_root_artifacts(scala_version, scala_major, is_scala_3) -> List[str]:
if is_scala_3:
root_artifacts.extend([
- f'org.scala-lang:scala3-library_3:{scala_version}',
+ # Versions of izumi-reflect greater than v2.2.1 depend on a version of the Scala standard library greater
+ # than v3.1.3. This is a problem because Scala v3.1.3, which we support, needs to use a matching version of
+ # the Scala standard library.
+ 'dev.zio:izumi-reflect_3:2.2.1',
+ f'org.jline:jline-reader:{JLINE_VERSION}',
+ f'org.jline:jline-terminal:{JLINE_VERSION}',
+ f'org.jline:jline-terminal-jna:{JLINE_VERSION}',
f'org.scala-lang:scala3-compiler_3:{scala_version}',
+ f'org.scala-lang:scala3-library_3:{scala_version}',
f'org.scala-lang:scala3-interfaces:{scala_version}',
+ f'org.scala-lang:scala3-tasty-inspector_3:{scala_version}',
f'org.scala-lang:tasty-core_3:{scala_version}',
- 'org.scala-sbt:compiler-interface:' +
- SBT_COMPILER_INTERFACE_VERSION,
+ f'org.scala-sbt:compiler-interface:{SBT_COMPILER_INTERFACE_VERSION}',
f'org.scala-sbt:util-interface:{SBT_UTIL_INTERFACE_VERSION}',
- f'org.jline:jline-reader:{JLINE_VERSION}',
- f'org.jline:jline-terminal:{JLINE_VERSION}',
- f'org.jline:jline-terminal-jna:{JLINE_VERSION}',
])
else:
diff --git a/specs2/specs2_junit.bzl b/specs2/specs2_junit.bzl
index d6fbf53da..359ac2d1e 100644
--- a/specs2/specs2_junit.bzl
+++ b/specs2/specs2_junit.bzl
@@ -13,6 +13,8 @@ load("//third_party/repositories:repositories.bzl", "repositories")
def specs2_junit_artifact_ids():
return [
"io_bazel_rules_scala_org_specs2_specs2_junit",
+ "org_portable_scala_portable_scala_reflect",
+ "org_scala_sbt_test_interface",
]
def specs2_junit_repositories(
diff --git a/src/java/io/bazel/rulesscala/specs2/Specs2RunnerBuilder.scala b/src/java/io/bazel/rulesscala/specs2/Specs2RunnerBuilder.scala
index 0acdf8454..52ad0118f 100644
--- a/src/java/io/bazel/rulesscala/specs2/Specs2RunnerBuilder.scala
+++ b/src/java/io/bazel/rulesscala/specs2/Specs2RunnerBuilder.scala
@@ -179,6 +179,6 @@ class FilteredSpecs2ClassRunner(parentRunner: org.specs2.runner.JUnitRunner, tes
private implicit class `Collection Regex Extensions`(coll: List[String]) {
def toRegexAlternation: Option[String] =
if (coll.isEmpty) None
- else Some(coll.map(_.toQuotedRegex).mkString("(", "|", ")"))
+ else Some(coll.map(_.toQuotedRegex).mkString("^(", "|", ")$"))
}
}
diff --git a/src/scala/io/bazel/rules_scala/dottyijar/BUILD b/src/scala/io/bazel/rules_scala/dottyijar/BUILD
new file mode 100644
index 000000000..e8dd594e7
--- /dev/null
+++ b/src/scala/io/bazel/rules_scala/dottyijar/BUILD
@@ -0,0 +1,42 @@
+load("//scala:scala.bzl", "scala_library_for_plugin_bootstrapping", "scala_specs2_junit_test")
+load("@rules_java//java:defs.bzl", "java_binary")
+
+scala_library_for_plugin_bootstrapping(
+ name = "dottyijar-library",
+ srcs = glob(
+ ["*.scala"],
+ exclude = ["*.spec.scala"],
+ ),
+ scala_version = "3.6.2",
+ visibility = ["//visibility:public"],
+ deps = [
+ "//src/scala/io/bazel/rules_scala/dottyijar/tasty",
+ "//src/scala/io/bazel/rules_scala/dottyijar/tasty/format",
+ "//src/scala/io/bazel/rules_scala/dottyijar/tasty/numeric",
+ "@dev_zio_izumi_reflect_3_6_2",
+ ],
+)
+
+# TODO: Eventually, we should make a bootstrapping toolchain so we don't have to create a `*_for_plugin_bootstrapping` equivalent for each Scala rule.
+java_binary(
+ name = "dottyijar",
+ main_class = "io.bazel.rules_scala.dottyijar.DottyIjar",
+ visibility = ["//visibility:public"],
+ runtime_deps = [":dottyijar-library"],
+)
+
+scala_specs2_junit_test(
+ name = "specs",
+ srcs = glob(["*.spec.scala"]),
+ scala_version = "3.6.2",
+ suffixes = ["Spec"],
+ deps = [
+ ":dottyijar-library",
+ "//src/scala/io/bazel/rules_scala/dottyijar/tasty:test",
+ "@io_bazel_rules_scala_org_specs2_specs2_common_3_6_2",
+ "@io_bazel_rules_scala_org_specs2_specs2_core_3_6_2",
+ "@io_bazel_rules_scala_org_specs2_specs2_junit",
+ "@io_bazel_rules_scala_org_specs2_specs2_matcher_3_6_2",
+ "@io_bazel_rules_scala_scala_tasty_inspector_3_6_2",
+ ],
+)
diff --git a/src/scala/io/bazel/rules_scala/dottyijar/DottyIjar.scala b/src/scala/io/bazel/rules_scala/dottyijar/DottyIjar.scala
new file mode 100644
index 000000000..3d0b721a9
--- /dev/null
+++ b/src/scala/io/bazel/rules_scala/dottyijar/DottyIjar.scala
@@ -0,0 +1,119 @@
+package io.bazel.rules_scala.dottyijar
+
+import io.bazel.rules_scala.dottyijar.tasty.Tasty
+import io.bazel.rules_scala.dottyijar.tasty.format.{TastyFormat, TastyReader, TastyWriter}
+import java.io.FileOutputStream
+import java.nio.file.{Path, Paths}
+import java.nio.file.attribute.FileTime
+import java.util.zip.{ZipEntry, ZipFile, ZipOutputStream}
+import scala.jdk.CollectionConverters.*
+
+object DottyIjar {
+ private def writeInterfaceJar(inputJar: ZipFile, outputStream: ZipOutputStream): Unit = {
+ def copyEntryWithContent(entry: ZipEntry, content: Array[Byte]): Unit = {
+ val newEntry = new ZipEntry(entry.getName)
+
+ newEntry.setCreationTime(FileTime.fromMillis(0))
+ newEntry.setLastAccessTime(FileTime.fromMillis(0))
+ newEntry.setLastModifiedTime(FileTime.fromMillis(0))
+
+ outputStream.putNextEntry(newEntry)
+ outputStream.write(content, 0, content.length)
+ }
+
+ def copyEntry(entry: ZipEntry): Unit = copyEntryWithContent(entry, inputJar.getInputStream(entry).readAllBytes())
+
+ outputStream.setComment(inputJar.getComment)
+ outputStream.setLevel(0)
+
+ val entryNames = inputJar.entries.asScala.map(_.getName).toSet
+
+ inputJar.entries.asScala.foreach {
+ case entry if entry.getName.startsWith("META-INF/") => copyEntry(entry)
+ case entry if entry.getName.endsWith(".class") =>
+ val i = entry.getName.lastIndexOf('/')
+ val directory = entry.getName.slice(0, i)
+ val filename = entry.getName.slice(i + 1, entry.getName.length)
+ val j = filename.indexOf("$")
+ val tastyFileBaseName = if (j == -1) filename.stripSuffix(".class") else filename.slice(0, j)
+
+ if (!entryNames(s"$directory/$tastyFileBaseName.tasty")) {
+ copyEntry(entry)
+ }
+
+ case entry if entry.getName.endsWith(".tasty") =>
+ val content = inputJar.getInputStream(entry).readAllBytes()
+ val updatedContent = TastyUpdater.updateTastyFile(content)
+
+ copyEntryWithContent(entry, updatedContent)
+
+ case entry => copyEntry(entry)
+ }
+ }
+
+ def main(arguments: Array[String]): Unit = Arguments
+ .parseArguments(arguments)
+ .fold(
+ println,
+ arguments => {
+ val inputJar = new ZipFile(arguments.inputPath.toFile)
+
+ try {
+ val outputStream = new ZipOutputStream(new FileOutputStream(arguments.outputPath.toFile))
+
+ try {
+ writeInterfaceJar(inputJar, outputStream)
+ } finally {
+ outputStream.close()
+ }
+ } finally {
+ inputJar.close()
+ }
+ },
+ )
+}
+
+private case class Arguments(inputPath: Path, outputPath: Path)
+
+object Arguments {
+ def parseArguments(arguments: Array[String]): Either[String, Arguments] = arguments
+ .foldLeft[Either[String, UnvalidatedArguments]](Right(UnvalidatedArguments())) {
+ case (unvalidatedArguments, argument) =>
+ unvalidatedArguments.flatMap { unvalidatedArguments =>
+ argument match {
+ case "-h" | "--help" =>
+ Left(
+ """dottyijar removes information from Scala 3 JARs that aren't needed for compilation.
+ |
+ |Usage:
+ | dottyijar