Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add scala_doc rule #760

Merged
merged 14 commits into from
May 31, 2019
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
43 changes: 43 additions & 0 deletions docs/scala_doc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# 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",
],
)

# 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` <br> A unique name for this target.
| deps | `List of labels, optional` <br> Labels for which you want to create scaladoc.
17 changes: 17 additions & 0 deletions scala/private/common.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
19 changes: 2 additions & 17 deletions scala/private/rule_impls.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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]

Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions scala/scala.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -700,3 +704,5 @@ def scala_specs2_junit_test(name, **kwargs):
suite_class = "io.bazel.rulesscala.specs2.Specs2DiscoveredTestSuite",
**kwargs
)

scala_doc = _scala_doc
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm following a pattern I see in other rules, in which you basically re-export rules that are loaded from other .bzl files, makes it such that a user can load multiple rules from the same .bzl file.

91 changes: 91 additions & 0 deletions scala/scala_doc.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
"""Scaladoc support"""

load("@io_bazel_rules_scala//scala/private:common.bzl", "collect_plugin_paths")

ScaladocAspectInfo = provider(fields = [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this can be private, add a _ suffix.

"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.
src_files = depset(direct = [file for file in ctx.rule.files.srcs if file.extension.lower() in ["java", "scala"]])

# Collect compile_jars from visited targets' deps.
compile_jars = depset(transitive = [dep[JavaInfo].compile_jars for dep in ctx.rule.attr.deps if JavaInfo 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("-d", output_path.path)
args.add_all(plugins, format_each = "-Xplugin:%s")
args.add_joined("-classpath", classpath, join_with = ":")
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],
),
"_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,
)
17 changes: 17 additions & 0 deletions src/scala/io/bazel/rules_scala/scaladoc_support/BUILD
Original file line number Diff line number Diff line change
@@ -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(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I put this target in src/scala/io/bazel/rules_scala/scaladoc_support because I initially had an actual Scala program do heavy lifting, but it turned out to be unnecessary and I could pass in everything through scaladoc/scalac args.

Let me know if this should be moved to the //scala package or something.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is fine.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool - happy to keep this here in case we do need to introduce a real Scala program.

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",
],
)
10 changes: 10 additions & 0 deletions test/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package(default_testonly = 1)
load(
"//scala:scala.bzl",
"scala_binary",
"scala_doc",
"scala_library",
"scala_test",
"scala_macro_library",
Expand Down Expand Up @@ -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"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
)
srcs = ["KindProjected.scala"],
plugins = ["@org_spire_math_kind_projector//jar"],
visibility = ["//visibility:public"],
)