diff --git a/README.md b/README.md index c9f01f2d5..eaf4be9e8 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ This project defines core build rules for [Scala](https://www.scala-lang.org/) t * [scalapb_proto_library](docs/scalapb_proto_library.md) * [scala_toolchain](docs/scala_toolchain.md) * [scala_import](docs/scala_import.md) +* [scala_doc](docs/scala_doc.md) ## Getting started diff --git a/docs/scala_doc.md b/docs/scala_doc.md new file mode 100644 index 000000000..5a7fe34cc --- /dev/null +++ b/docs/scala_doc.md @@ -0,0 +1,48 @@ +# scala_doc + +```python +scala_binary( + name, + deps, +) +``` + +`scala_doc` generates [Scaladoc](https://docs.scala-lang.org/style/scaladoc.html) for sources +for targets, including sources from upstream deps. Readily hostable HTML is written to a `name.html` output folder. + +Scaladoc can be somewhat slow to build. In that case, you can tell Bazel to build this target manually, +i.e. only when named explicitly and not through wildcards: `tags = ["manual"]`. + +## Example + +```python +scala_doc( + name = "scala_docs", + tags = ["manual"], + deps = [ + ":target1", + ":target2", + ":anothertarget", + ], + scalacopts = [ + "-Ypartial-unification", + "-Ywarn-unused-import", + ], +) + +# Use pkg_tar to tarball up +# https://docs.bazel.build/versions/master/be/pkg.html#pkg_tar +pkg_tar( + name = "scala_docs_archive", + srcs = [":scala_docs"], + extension = "tar.gz", +) +``` + +## Attributes + +| Attribute name | Description | +| --------------------- | ----------------------------------------------------- | +| name | `Name, required`
A unique name for this target. +| deps | `List of labels, optional`
Labels for which you want to create scaladoc. +| scalacopts | `List of strings, optional`
Extra compiler options for this library to be passed to scalac. \ No newline at end of file diff --git a/scala/private/common.bzl b/scala/private/common.bzl index 2cea9ffd3..4fcabc2f6 100644 --- a/scala/private/common.bzl +++ b/scala/private/common.bzl @@ -36,6 +36,23 @@ def collect_jars( else: return _collect_jars_when_dependency_analyzer_is_on(dep_targets) +def collect_plugin_paths(plugins): + """Get the actual jar paths of plugins as a depset.""" + paths = [] + for p in plugins: + if hasattr(p, "path"): + paths.append(p) + elif hasattr(p, "scala"): + paths.extend([j.class_jar for j in p.scala.outputs.jars]) + elif hasattr(p, "java"): + paths.extend([j.class_jar for j in p.java.outputs.jars]) + # support http_file pointed at a jar. http_jar uses ijar, + # which breaks scala macros + + elif hasattr(p, "files"): + paths.extend([f for f in p.files if not_sources_jar(f.basename)]) + return depset(paths) + def _collect_jars_when_dependency_analyzer_is_off( dep_targets, unused_dependency_checker_is_off, diff --git a/scala/private/rule_impls.bzl b/scala/private/rule_impls.bzl index 0d2c4ce8c..216dd97b2 100644 --- a/scala/private/rule_impls.bzl +++ b/scala/private/rule_impls.bzl @@ -26,6 +26,7 @@ load( ":common.bzl", "add_labels_of_jars_to", "collect_jars", + "collect_plugin_paths", "collect_srcjars", "create_java_provider", "not_sources_jar", @@ -127,22 +128,6 @@ touch {statsfile} arguments = [], ) -def _collect_plugin_paths(plugins): - paths = [] - for p in plugins: - if hasattr(p, "path"): - paths.append(p) - elif hasattr(p, "scala"): - paths.extend([j.class_jar for j in p.scala.outputs.jars]) - elif hasattr(p, "java"): - paths.extend([j.class_jar for j in p.java.outputs.jars]) - # support http_file pointed at a jar. http_jar uses ijar, - # which breaks scala macros - - elif hasattr(p, "files"): - paths.extend([f for f in p.files if not_sources_jar(f.basename)]) - return depset(paths) - def _expand_location(ctx, flags): return [ctx.expand_location(f, ctx.attr.data) for f in flags] @@ -172,7 +157,7 @@ def compile_scala( unused_dependency_checker_mode = "off", unused_dependency_checker_ignored_targets = []): # look for any plugins: - plugins = _collect_plugin_paths(plugins) + plugins = collect_plugin_paths(plugins) internal_plugin_jars = [] dependency_analyzer_mode = "off" compiler_classpath_jars = cjars diff --git a/scala/scala.bzl b/scala/scala.bzl index 375fa0d1c..f85b5dd78 100644 --- a/scala/scala.bzl +++ b/scala/scala.bzl @@ -32,6 +32,10 @@ load( "@io_bazel_rules_scala//scala:plusone.bzl", _collect_plus_one_deps_aspect = "collect_plus_one_deps_aspect", ) +load( + "@io_bazel_rules_scala//scala:scala_doc.bzl", + _scala_doc = "scala_doc", +) _launcher_template = { "_java_stub_template": attr.label( @@ -700,3 +704,5 @@ def scala_specs2_junit_test(name, **kwargs): suite_class = "io.bazel.rulesscala.specs2.Specs2DiscoveredTestSuite", **kwargs ) + +scala_doc = _scala_doc \ No newline at end of file diff --git a/scala/scala_doc.bzl b/scala/scala_doc.bzl new file mode 100644 index 000000000..2aed41c78 --- /dev/null +++ b/scala/scala_doc.bzl @@ -0,0 +1,103 @@ +"""Scaladoc support""" + +load("@io_bazel_rules_scala//scala/private:common.bzl", "collect_plugin_paths") + +_ScaladocAspectInfo = provider(fields = [ + "src_files", + "compile_jars", + "plugins", +]) + +def _scaladoc_aspect_impl(target, ctx): + """Collect source files and compile_jars from JavaInfo-returning deps.""" + + # We really only care about visited targets with srcs, so only look at those. + if hasattr(ctx.rule.attr, "srcs"): + # Collect only Java and Scala sources enumerated in visited targets, including src_files in deps. + src_files = depset( + direct = [file for file in ctx.rule.files.srcs if file.extension.lower() in ["java", "scala"]], + transitive = [dep[_ScaladocAspectInfo].src_files for dep in ctx.rule.attr.deps if _ScaladocAspectInfo in dep], + ) + + # Collect compile_jars from visited targets' deps. + compile_jars = depset( + direct = [file for file in ctx.rule.files.deps], + transitive = ( + [dep[JavaInfo].compile_jars for dep in ctx.rule.attr.deps if JavaInfo in dep] + + [dep[_ScaladocAspectInfo].compile_jars for dep in ctx.rule.attr.deps if _ScaladocAspectInfo in dep] + ), + ) + + plugins = depset() + if hasattr(ctx.rule.attr, "plugins"): + plugins = depset(direct = ctx.rule.attr.plugins) + + return [_ScaladocAspectInfo( + src_files = src_files, + compile_jars = compile_jars, + plugins = plugins, + )] + else: + return [] + +_scaladoc_aspect = aspect( + implementation = _scaladoc_aspect_impl, + attr_aspects = ["deps"], + required_aspect_providers = [ + [JavaInfo], + ], +) + +def _scala_doc_impl(ctx): + # scaladoc warns if you don't have the output directory already created, which is annoying. + output_path = ctx.actions.declare_directory("{}.html".format(ctx.attr.name)) + + # Collect all source files and compile_jars to pass to scaladoc by way of an aspect. + src_files = depset(transitive = [dep[_ScaladocAspectInfo].src_files for dep in ctx.attr.deps]) + compile_jars = depset(transitive = [dep[_ScaladocAspectInfo].compile_jars for dep in ctx.attr.deps]) + + # Get the 'real' paths to the plugin jars. + plugins = collect_plugin_paths(depset(transitive = [dep[_ScaladocAspectInfo].plugins for dep in ctx.attr.deps])) + + # Construct the full classpath depset since we need to add compiler plugins too. + classpath = depset(transitive = [plugins, compile_jars]) + + # Construct scaladoc args, which also include scalac args. + # See `scaladoc -help` for more information. + args = ctx.actions.args() + args.add("-usejavacp") + args.add("-nowarn") # turn off warnings for now since they can obscure actual errors for large scala_doc targets + args.add_all(ctx.attr.scalacopts) + args.add("-d", output_path.path) + args.add_all(plugins, format_each = "-Xplugin:%s") + args.add_joined("-classpath", classpath, join_with = ctx.configuration.host_path_separator) + args.add_all(src_files) + + # Run the scaladoc tool! + ctx.actions.run( + inputs = depset(transitive = [src_files, classpath]), + outputs = [output_path], + executable = ctx.attr._scaladoc.files_to_run.executable, + mnemonic = "ScalaDoc", + progress_message = "scaladoc {}".format(ctx.label), + arguments = [args], + ) + + return [DefaultInfo(files = depset(direct = [output_path]))] + +scala_doc = rule( + attrs = { + "deps": attr.label_list( + aspects = [_scaladoc_aspect], + providers = [JavaInfo], + ), + "scalacopts": attr.string_list(), + "_scaladoc": attr.label( + cfg = "host", + executable = True, + default = Label("//src/scala/io/bazel/rules_scala/scaladoc_support:scaladoc_generator"), + ), + }, + doc = "Generate Scaladoc HTML documentation for source files in from the given dependencies.", + implementation = _scala_doc_impl, +) diff --git a/src/scala/io/bazel/rules_scala/scaladoc_support/BUILD b/src/scala/io/bazel/rules_scala/scaladoc_support/BUILD new file mode 100644 index 000000000..b48ad5fb4 --- /dev/null +++ b/src/scala/io/bazel/rules_scala/scaladoc_support/BUILD @@ -0,0 +1,17 @@ +load("//scala:scala.bzl", "scala_binary") + +# A simple scala_binary to run scaladoc. +# `bazel run` this target with "-help" as a param for usage text: +# bazel run -- "//src/scala/io/bazel/rules_scala/scaladoc_support:scaladoc_generator" -help +scala_binary( + name = "scaladoc_generator", + main_class = "scala.tools.nsc.ScalaDoc", + visibility = ["//visibility:public"], + runtime_deps = [ + "//external:io_bazel_rules_scala/dependency/scala/parser_combinators", + "//external:io_bazel_rules_scala/dependency/scala/scala_compiler", + "//external:io_bazel_rules_scala/dependency/scala/scala_library", + "//external:io_bazel_rules_scala/dependency/scala/scala_reflect", + "//external:io_bazel_rules_scala/dependency/scala/scala_xml", + ], +) diff --git a/test/BUILD b/test/BUILD index 319d20edc..a1be14ae4 100644 --- a/test/BUILD +++ b/test/BUILD @@ -3,6 +3,7 @@ package(default_testonly = 1) load( "//scala:scala.bzl", "scala_binary", + "scala_doc", "scala_library", "scala_test", "scala_macro_library", @@ -137,6 +138,15 @@ scala_library( deps = ["ExportOnly"], ) +scala_doc( + name = "ScalaDoc", + deps = [ + ":HelloLib", + ":OtherLib", + "//test/src/main/scala/scalarules/test/compiler_plugin", # brings kind-projector compiler plugin with it + ] +) + scala_library( name = "UnusedLib", srcs = ["UnusedLib.scala"] diff --git a/test/src/main/scala/scalarules/test/compiler_plugin/BUILD.bazel b/test/src/main/scala/scalarules/test/compiler_plugin/BUILD.bazel index 368ae414c..53db39a9b 100644 --- a/test/src/main/scala/scalarules/test/compiler_plugin/BUILD.bazel +++ b/test/src/main/scala/scalarules/test/compiler_plugin/BUILD.bazel @@ -2,6 +2,7 @@ load("//scala:scala.bzl", "scala_library") scala_library( name = "compiler_plugin", - srcs = [ "KindProjected.scala" ], - plugins = ["@org_spire_math_kind_projector//jar"] -) \ No newline at end of file + srcs = ["KindProjected.scala"], + plugins = ["@org_spire_math_kind_projector//jar"], + visibility = ["//visibility:public"], +)