From 7abf6c615340998bf5dac182186ea19bf05fc93c Mon Sep 17 00:00:00 2001 From: Yusup <72071763+name-snrl@users.noreply.github.com> Date: Mon, 18 Dec 2023 17:16:59 +0500 Subject: [PATCH] Add mezel support (#28) * fix: implementation of `enable_semanticdb` attribute * chore: add TODO +reformat scala3 dir * feat: add `semanticdb_bundle_in_jar` attribute to toolchain Also, semanticdb generation has been moved to the zinc compile phase * feat: add `scala_version` attribute to `scala_toolchain` * feat: add compatibility for rules with the `scala` attribute * feat: add mezel compatibility rules * fix: add generation of stub diagnostics --- WORKSPACE | 13 ++ mezel_compatibility/BUILD.bazel | 0 mezel_compatibility/aspect.bzl | 178 ++++++++++++++++++++ mezel_compatibility/repositories.bzl | 31 ++++ rules/common/private/get_toolchain.bzl | 17 +- rules/private/phases/phase_zinc_compile.bzl | 34 +++- rules/providers.bzl | 8 + rules/scala/workspace.bzl | 2 +- scala3/private/toolchain_constants.bzl | 4 + scala3/private/worker_scala_binary.bzl | 2 +- scala3/private/worker_scala_library.bzl | 2 +- scala3/repositories.bzl | 11 +- scala3/toolchain.bzl | 25 +-- 13 files changed, 307 insertions(+), 20 deletions(-) create mode 100644 mezel_compatibility/BUILD.bazel create mode 100644 mezel_compatibility/aspect.bzl create mode 100644 mezel_compatibility/repositories.bzl diff --git a/WORKSPACE b/WORKSPACE index 0da119fb..9d3ed345 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -20,6 +20,7 @@ maven_install( "org.scala-sbt:librarymanagement-core_3:2.0.0-alpha12", "org.scala-sbt:librarymanagement-coursier_3:2.0.0-alpha6", ], + fetch_sources = True, maven_install_json = "//:deps_install.json", repositories = [ "https://repo1.maven.org/maven2", @@ -54,6 +55,18 @@ load("//3rdparty:workspace.bzl", "maven_dependencies") maven_dependencies() +load("//mezel_compatibility:repositories.bzl", "mezel_compatibility_repository") + +mezel_compatibility_repository( + name = "mezel", + mezel_version = "216327ab2fc6d5866f13ace1bf75c9d1abdcd8a6", + sha256 = "dbdb144fc943670dc1b715629f939d8f5010ae1b2ab889b3620866ce19cda1df", +) + +load("@mezel//rules:load_mezel.bzl", "load_mezel") + +load_mezel() + load("//rules/scalafmt:workspace.bzl", "scalafmt_default_config", "scalafmt_repositories") scalafmt_repositories() diff --git a/mezel_compatibility/BUILD.bazel b/mezel_compatibility/BUILD.bazel new file mode 100644 index 00000000..e69de29b diff --git a/mezel_compatibility/aspect.bzl b/mezel_compatibility/aspect.bzl new file mode 100644 index 00000000..0f1749c0 --- /dev/null +++ b/mezel_compatibility/aspect.bzl @@ -0,0 +1,178 @@ +"Mezel aspect compatible with `rules_scala3`" + +load("@rules_scala3//rules:providers.bzl", "ScalaInfo", "SemanticdbInfo") + +BuildTargetInfo = provider( + doc = "Generate depset of bypassed targets.", + fields = { + "output": "output", + }, +) + +# should only apply to rules that generate semanticdb (phase_zinc_compile) +# also all rules must contain attrs, which are used here + +#"scala_library", +#"scala_binary", +#"scala_test", + +#"scalajs_library", # may not work as is +#"scalajs_link", # may not work as is + +def _mezel_aspect_impl(target, ctx): + # TODO check rules + #print(ctx.rule.kind) + + jdk = ctx.attr._jdk + target_attrs = ctx.rule.attr + toolchain = target[ScalaInfo].toolchain + + if not toolchain.enable_semanticdb: + fail("SemanticDB is not enabled, please set the `enable_semanticdb` attribute to `True` in your `scala_toolchain`", toolchain) + + if toolchain.semanticdb_bundle_in_jar: + fail("SemanticDB is bundled in the output jar, please generate it separately by setting the `semanticdb_bundle_in_jar` attribute to `False` in your `scala_toolchain`") + + toolchain_opts = toolchain.global_scalacopts if toolchain.global_scalacopts else [] + target_opts = target_attrs.scalacopts if target_attrs.scalacopts else [] + + opts = toolchain_opts + target_opts + + compiler_version = toolchain.scala_version + + semanticdb_target_root = target[SemanticdbInfo].target_root + + scala_compile_classpath = [ + file + for target in toolchain.compiler_classpath + for file in target[JavaInfo].compile_jars.to_list() + ] + + dep_outputs = [ + dependency[BuildTargetInfo].output + for dependency in target_attrs.deps + if BuildTargetInfo in dependency + ] + direct_dep_labels = [dependency.label for dependency in dep_outputs] + + transitive_labels = depset( + [target.label], + transitive = [x.transitive_labels for x in dep_outputs], + ) + ignore = transitive_labels.to_list() + + output_class_jars = [x.class_jar.path for x in target[JavaInfo].java_outputs] + if (len(output_class_jars) != 1): + fail("Expected exactly one output class jar, got {}".format(output_class_jars)) + output_class_jar = output_class_jars[0] + + transitive_compile_jars = target[JavaInfo].transitive_compile_time_jars.to_list() + cp_jars = [x.path for x in transitive_compile_jars if x.owner != target.label] + transitive_source_jars = target[JavaInfo].transitive_source_jars.to_list() + src_jars = [x.path for x in transitive_source_jars if x.owner not in ignore] + + raw_plugins = target_attrs.plugins if target_attrs.plugins else [] + plugins = [ + y.path + for x in raw_plugins + if JavaInfo in x + for y in x[JavaInfo].compile_jars.to_list() + ] + + # generate bsp_info files + scalac_options_file = ctx.actions.declare_file("{}_bsp_scalac_options.json".format(target.label.name)) + scalac_options_content = struct( + scalacopts = opts, + semanticdbPlugin = "", + plugins = plugins, + classpath = cp_jars, + targetroot = semanticdb_target_root, + outputClassJar = output_class_jar, + compilerVersion = compiler_version, + ) + ctx.actions.write(scalac_options_file, json.encode(scalac_options_content)) + + sources_file = ctx.actions.declare_file("{}_bsp_sources.json".format(target.label.name)) + sources_content = struct( + sources = [ + f.path + for src in target_attrs.srcs + for f in src.files.to_list() + ], + ) + ctx.actions.write(sources_file, json.encode(sources_content)) + + dependency_sources_file = ctx.actions.declare_file("{}_bsp_dependency_sources.json".format(target.label.name)) + dependency_sources_content = struct( + sourcejars = src_jars, + ) + ctx.actions.write(dependency_sources_file, json.encode(dependency_sources_content)) + + build_target_file = ctx.actions.declare_file("{}_bsp_build_target.json".format(target.label.name)) + build_target_content = struct( + javaHome = jdk[java_common.JavaRuntimeInfo].java_home, + scalaCompilerClasspath = [x.path for x in scala_compile_classpath], + compilerVersion = compiler_version, + deps = [str(label) for label in direct_dep_labels], + directory = target.label.package, + ) + ctx.actions.write(build_target_file, json.encode(build_target_content)) + + ctx.actions.do_nothing( + mnemonic = "MezelAspect", + inputs = [scalac_options_file, sources_file, dependency_sources_file, build_target_file], + ) + + files = struct( + label = target.label, + transitive_labels = transitive_labels, + ) + + transitive_output_files = [ + dependency[OutputGroupInfo].bsp_info + for dependency in target_attrs.deps + if OutputGroupInfo in dependency and hasattr(dependency[OutputGroupInfo], "bsp_info") + ] + + # TODO: add .diagnosticsproto generation to rules and it would be + # nice to move this to a separate phase along with semanticdb generation + diagnostics = ctx.actions.declare_file("{}.diagnosticsproto".format(ctx.label.name)) + ctx.actions.run_shell( + mnemonic = "Scalac", + command = "touch $1", + arguments = [diagnostics.path], + outputs = [diagnostics], + ) + + return [ + OutputGroupInfo( + bsp_info = depset( + [scalac_options_file, sources_file, dependency_sources_file, build_target_file, diagnostics], + transitive = transitive_output_files, + ), + bsp_info_deps = depset( + scala_compile_classpath, + transitive = [ + target[JavaInfo].transitive_compile_time_jars, + target[JavaInfo].transitive_source_jars, + ] + [x[JavaInfo].compile_jars for x in raw_plugins], + ), + ), + BuildTargetInfo(output = files), + ] + +mezel_aspect = aspect( + implementation = _mezel_aspect_impl, + attr_aspects = ["deps"], + required_aspect_providers = [[JavaInfo, SemanticdbInfo]], + required_providers = [ + [JavaInfo, ScalaInfo], # allow non-semanticdb targets to throw an error + [JavaInfo, SemanticdbInfo, ScalaInfo], + ], + attrs = { + "_jdk": attr.label( + default = Label("@bazel_tools//tools/jdk:current_java_runtime"), + providers = [java_common.JavaRuntimeInfo], + ), + }, +) diff --git a/mezel_compatibility/repositories.bzl b/mezel_compatibility/repositories.bzl new file mode 100644 index 00000000..6fb68665 --- /dev/null +++ b/mezel_compatibility/repositories.bzl @@ -0,0 +1,31 @@ +"""Repository rule for loading mazel repository""" + +def _mezel_compatibility_repository_impl(repository_ctx): + repository_ctx.download_and_extract( + url = "https://github.com/valdemargr/mezel/archive/%s.zip" % repository_ctx.attr.mezel_version, + sha256 = repository_ctx.attr.sha256, + type = "zip", + stripPrefix = "mezel-%s" % repository_ctx.attr.mezel_version, + ) + replacement_path = "aspects/aspect.bzl" + repository_ctx.delete(replacement_path) + repository_ctx.file( + replacement_path, + content = repository_ctx.read(Label("//mezel_compatibility:aspect.bzl")), + executable = False, + ) + +mezel_compatibility_repository = repository_rule( + doc = "A repository rule that loads the mazel repository, adding compatibility with `rules_scala3`.", + attrs = { + "mezel_version": attr.string( + doc = "The latest version can be found in the README of the upstream repository.", + mandatory = True, + ), + "sha256": attr.string( + doc = "The hash of the latest version can be found in the README of the upstream repository.", + mandatory = True, + ), + }, + implementation = _mezel_compatibility_repository_impl, +) diff --git a/rules/common/private/get_toolchain.bzl b/rules/common/private/get_toolchain.bzl index ceda9446..de4620b6 100644 --- a/rules/common/private/get_toolchain.bzl +++ b/rules/common/private/get_toolchain.bzl @@ -9,8 +9,11 @@ load( _ZincConfiguration = "ZincConfiguration", ) -def _gen_toolchain(scala): +def _gen_toolchain(scala, toolchain): toolchain_info = platform_common.ToolchainInfo( + scala_version = scala[_ScalaConfiguration].version, + enable_semanticdb = toolchain.enable_semanticdb, + semanticdb_bundle_in_jar = toolchain.semanticdb_bundle_in_jar, is_zinc = True if _ZincConfiguration in scala else False, zinc_log_level = scala[_ZincConfiguration].log_level if _ZincConfiguration in scala else None, compiler_bridge = scala[_ZincConfiguration].compiler_bridge if _ZincConfiguration in scala else None, @@ -42,9 +45,15 @@ def get_toolchain(ctx): """ if getattr(ctx.attr, "_worker_rule", False): - return _gen_toolchain(ctx.attr._scala) + stub_toolchain = platform_common.ToolchainInfo( + enable_semanticdb = False, + semanticdb_bundle_in_jar = False, + ) + return _gen_toolchain(ctx.attr._scala, stub_toolchain) + + toolchain = ctx.toolchains["@rules_scala3//scala3:toolchain_type"] if getattr(ctx.attr, "scala", False): - return _gen_toolchain(ctx.attr.scala) + return _gen_toolchain(ctx.attr.scala, toolchain) - return ctx.toolchains["@rules_scala3//scala3:toolchain_type"] + return toolchain diff --git a/rules/private/phases/phase_zinc_compile.bzl b/rules/private/phases/phase_zinc_compile.bzl index 909a3421..15a4524b 100644 --- a/rules/private/phases/phase_zinc_compile.bzl +++ b/rules/private/phases/phase_zinc_compile.bzl @@ -1,5 +1,7 @@ +load("@bazel_skylib//lib:paths.bzl", "paths") load( "@rules_scala3//rules:providers.bzl", + _SemanticdbInfo = "SemanticdbInfo", _ZincInfo = "ZincInfo", ) load( @@ -17,6 +19,35 @@ load("//rules/common:private/get_toolchain.bzl", "get_toolchain") def phase_zinc_compile(ctx, g): toolchain = get_toolchain(ctx) + semanticdb_files = [] + semanticdb_scalacopts = [] + if toolchain.enable_semanticdb: + semanticdb_scalacopts.append("-Xsemanticdb") + + target_output_path = paths.dirname(ctx.outputs.jar.path) + + semanticdb_intpath = "_scalac/" + ctx.label.name + "/classes" if toolchain.semanticdb_bundle_in_jar == True else "_semanticdb/" + ctx.label.name + + semanticdb_target_root = "%s/%s" % (target_output_path, semanticdb_intpath) + + if not toolchain.semanticdb_bundle_in_jar: + semanticdb_scalacopts.append("-semanticdb-target:" + semanticdb_target_root) + + # declare all the semanticdb files + semanticdb_outpath = "META-INF/semanticdb" + + for source_file in ctx.files.srcs: + if source_file.extension == "scala": + output_filename = "%s/%s/%s.semanticdb" % (semanticdb_intpath, semanticdb_outpath, source_file.path) + semanticdb_files.append(ctx.actions.declare_file(output_filename)) + + semanticdb_info = _SemanticdbInfo( + semanticdb_enabled = True, + target_root = None if toolchain.semanticdb_bundle_in_jar else semanticdb_target_root, + is_bundled_in_jar = toolchain.semanticdb_bundle_in_jar, + ) + g.out.providers.append(semanticdb_info) + apis = ctx.actions.declare_file("{}/apis.gz".format(ctx.label.name)) infos = ctx.actions.declare_file("{}/infos.gz".format(ctx.label.name)) mains_file = ctx.actions.declare_file("{}.jar.mains.txt".format(ctx.label.name)) @@ -43,6 +74,7 @@ def phase_zinc_compile(ctx, g): args.add_all(g.classpaths.compile, format_each = "--cp=%s") args.add_all(toolchain.global_scalacopts, format_each = "--compiler_option=%s") args.add_all(ctx.attr.scalacopts, format_each = "--compiler_option=%s") + args.add_all(semanticdb_scalacopts, format_each = "--compiler_option=%s") args.add_all(javacopts, format_each = "--java_compiler_option=%s") args.add(ctx.label, format = "--label=%s") args.add("--main_manifest", mains_file) @@ -74,7 +106,7 @@ def phase_zinc_compile(ctx, g): ] + [zinc.deps_files for zinc in zincs], ) - outputs = [g.classpaths.jar, mains_file, apis, infos, relations, setup, stamps, used, tmp] + outputs = [g.classpaths.jar, mains_file, apis, infos, relations, setup, stamps, used, tmp] + semanticdb_files # todo: different execution path for nosrc jar? ctx.actions.run( diff --git a/rules/providers.bzl b/rules/providers.bzl index afba7e76..7d7a2260 100644 --- a/rules/providers.bzl +++ b/rules/providers.bzl @@ -193,3 +193,11 @@ LabeledJars = provider( "values": "The preorder depset of label and jars.", }, ) + +SemanticdbInfo = provider( + fields = { + "semanticdb_enabled": "boolean", + "target_root": "directory containing the semanticdb files (relative to execroot).", + "is_bundled_in_jar": "boolean: whether the semanticdb files are bundled inside the jar", + }, +) diff --git a/rules/scala/workspace.bzl b/rules/scala/workspace.bzl index 059915a2..a07c7fc2 100644 --- a/rules/scala/workspace.bzl +++ b/rules/scala/workspace.bzl @@ -100,7 +100,7 @@ def scala_repositories(): ["bazel_skylib", None, "https://github.com/bazelbuild/bazel-skylib/releases/download/{version}/bazel-skylib-{version}.tar.gz".format(version = skylib_tag), "74d544d96f4a5bb630d465ca8bbcfe231e3594e5aae57e1edbf17a6eb3ca2506"], ["com_google_protobuf", "protobuf-" + protobuf_tag, "https://github.com/protocolbuffers/protobuf/archive/v{}.tar.gz".format(protobuf_tag), "22fdaf641b31655d4b2297f9981fa5203b2866f8332d3c6333f6b0107bb320de"], ["rules_proto", "rules_proto-" + rules_proto_tag, "https://github.com/bazelbuild/rules_proto/archive/{}.tar.gz".format(rules_proto_tag), "dc3fb206a2cb3441b485eb1e423165b231235a1ea9b031b4433cf7bc1fa460dd"], - ["rules_python", "rules_python-" + rules_python_tag, "https://github.com/bazelbuild/rules_python/releases/download/{version}/rules_python-{version}.tar.gz".format(version = rules_python_tag)], + ["rules_python", "rules_python-" + rules_python_tag, "https://github.com/bazelbuild/rules_python/releases/download/{version}/rules_python-{version}.tar.gz".format(version = rules_python_tag), "ffc7b877c95413c82bfd5482c017edcf759a6250d8b24e82f41f3c8b8d9e287e"], ] for dep in rules_deps: maybe(http_archive, name = dep[0], strip_prefix = dep[1], url = dep[2], sha256 = dep[3] if len(dep) == 4 else "") diff --git a/scala3/private/toolchain_constants.bzl b/scala3/private/toolchain_constants.bzl index a846dd7f..8c2ea89a 100644 --- a/scala3/private/toolchain_constants.bzl +++ b/scala3/private/toolchain_constants.bzl @@ -5,6 +5,10 @@ SCALA_TOOLCHAIN_ATTRS = { default = False, doc = "Enable SemanticDB.", ), + "semanticdb_bundle_in_jar": attr.bool( + default = False, + doc = "Whether semanticdb should be generated inside the output jar file or separately.", + ), "is_zinc": attr.bool( default = True, doc = "Use zinc compiler?", diff --git a/scala3/private/worker_scala_binary.bzl b/scala3/private/worker_scala_binary.bzl index bcf55825..e7703236 100644 --- a/scala3/private/worker_scala_binary.bzl +++ b/scala3/private/worker_scala_binary.bzl @@ -1,7 +1,6 @@ "A module defining `worker_scala_binary` rules" load("@bazel_skylib//lib:dicts.bzl", _dicts = "dicts") -load("//rules:providers.bzl", _ScalaConfiguration = "ScalaConfiguration") load("//rules:jvm.bzl", _labeled_jars = "labeled_jars") load( "//rules:private_proxy.bzl", @@ -17,6 +16,7 @@ load( _phase_singlejar = "phase_singlejar", _run_phases = "run_phases", ) +load("//rules:providers.bzl", _ScalaConfiguration = "ScalaConfiguration") _compile_private_attributes = { "_java_toolchain": attr.label( diff --git a/scala3/private/worker_scala_library.bzl b/scala3/private/worker_scala_library.bzl index b03837e5..a857e2a2 100644 --- a/scala3/private/worker_scala_library.bzl +++ b/scala3/private/worker_scala_library.bzl @@ -1,7 +1,6 @@ "A module defining `worker_scala_library` rules" load("@bazel_skylib//lib:dicts.bzl", _dicts = "dicts") -load("//rules:providers.bzl", _ScalaConfiguration = "ScalaConfiguration") load("//rules:jvm.bzl", _labeled_jars = "labeled_jars") load( "//rules:private_proxy.bzl", @@ -16,6 +15,7 @@ load( _phase_singlejar = "phase_singlejar", _run_phases = "run_phases", ) +load("//rules:providers.bzl", _ScalaConfiguration = "ScalaConfiguration") _compile_private_attributes = { "_java_toolchain": attr.label( diff --git a/scala3/repositories.bzl b/scala3/repositories.bzl index d953df96..a599f017 100644 --- a/scala3/repositories.bzl +++ b/scala3/repositories.bzl @@ -98,8 +98,10 @@ def _scala3_toolchain_repository_impl(repository_ctx): repository_ctx.name, )) - # TODO load maven deps, like `rules_scala_toolchain_deps_repositories` does - # `scala_version` should be used to resolve deps + # TODO load maven deps, like `rules_scala_toolchain_deps_repositories` does. + # `repository_ctx.attr.scala_version` should be used to resolve deps and + # determine the full version + scala_version = "3.3.1" compiler_bridge = repository_ctx.attr.compiler_bridge or "@scala_sbt_bridge_3_3_1//jar" compiler_classpath = repository_ctx.attr.compiler_classpath or [ @@ -116,6 +118,7 @@ def _scala3_toolchain_repository_impl(repository_ctx): "@scala_library_2_13_11//jar", ] + # TODO replace with repository_ctx.template() build_content = """#Generated by scala3/repositories.bzl load("@rules_scala3//scala3:toolchain.bzl", "scala_toolchain") load("@bazel_skylib//rules:common_settings.bzl", "string_flag") @@ -142,7 +145,9 @@ config_setting(name = "deps_used_error", flag_values = {{ ":deps_used": "error" scala_toolchain( name = "toolchain_impl", + scala_version = "{scala_version}", enable_semanticdb = {enable_semanticdb}, + semanticdb_bundle_in_jar = {semanticdb_bundle_in_jar}, is_zinc = {is_zinc}, zinc_log_level = "{zinc_log_level}", compiler_bridge = "{compiler_bridge}", @@ -171,7 +176,9 @@ toolchain( """ repository_ctx.file("BUILD.bazel", build_content.format( + scala_version = scala_version, enable_semanticdb = repository_ctx.attr.enable_semanticdb, + semanticdb_bundle_in_jar = repository_ctx.attr.semanticdb_bundle_in_jar, is_zinc = repository_ctx.attr.is_zinc, zinc_log_level = repository_ctx.attr.zinc_log_level, compiler_bridge = compiler_bridge, diff --git a/scala3/toolchain.bzl b/scala3/toolchain.bzl index fa6da81f..e496de0e 100644 --- a/scala3/toolchain.bzl +++ b/scala3/toolchain.bzl @@ -1,15 +1,15 @@ """This module implements the `scala_toolchain`.""" -load( - "//scala3/private:toolchain_constants.bzl", - _toolchain_attrs = "SCALA_TOOLCHAIN_ATTRS", -) load( "//rules:private_proxy.bzl", _phase_bootstrap_compile = "phase_bootstrap_compile", _phase_zinc_compile = "phase_zinc_compile", _phase_zinc_depscheck = "phase_zinc_depscheck", ) +load( + "//scala3/private:toolchain_constants.bzl", + _toolchain_attrs = "SCALA_TOOLCHAIN_ATTRS", +) def _scala_toolchain_impl(ctx): if ctx.attr.is_zinc: @@ -22,10 +22,6 @@ def _scala_toolchain_impl(ctx): ("=", "compile", "compile", _phase_bootstrap_compile), ] - global_scalacopts = ctx.attr.global_scalacopts - if ctx.attr.enable_semanticdb: - global_scalacopts.append("-Xsemanticdb") - if not ctx.attr.deps_direct in ["off", "warn", "error"]: fail("Argument `deps_direct` of `scala_toolchains` must be one of off, warn, error.") @@ -33,13 +29,16 @@ def _scala_toolchain_impl(ctx): fail("Argument `deps_used` of `scala_toolchains` must be one of off, warn, error.") toolchain_info = platform_common.ToolchainInfo( + scala_version = ctx.attr.scala_version, + enable_semanticdb = ctx.attr.enable_semanticdb, + semanticdb_bundle_in_jar = ctx.attr.semanticdb_bundle_in_jar, is_zinc = ctx.attr.is_zinc, zinc_log_level = ctx.attr.zinc_log_level, compiler_bridge = ctx.file.compiler_bridge, compiler_classpath = ctx.attr.compiler_classpath, runtime_classpath = ctx.attr.runtime_classpath, global_plugins = ctx.attr.global_plugins, - global_scalacopts = global_scalacopts, + global_scalacopts = ctx.attr.global_scalacopts, global_jvm_flags = ctx.attr.global_jvm_flags, phases = phases, compile_worker = ctx.attr._compile_worker, @@ -52,7 +51,13 @@ def _scala_toolchain_impl(ctx): scala_toolchain = rule( implementation = _scala_toolchain_impl, - attrs = _toolchain_attrs, + attrs = dict( + scala_version = attr.string( + mandatory = True, + doc = "Scala version.", + ), + **_toolchain_attrs + ), doc = """Declares a Scala toolchain. Example: