blob: 4c407755f9940ed54f174273452a7f853027ea2d [file]
# Copyright 2018 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Rules for generating documentation with `rustdoc` for Bazel built crates"""
load("//rust/private:common.bzl", "rust_common")
load("//rust/private:pic_utils.bzl", "should_use_pic")
load("//rust/private:providers.bzl", "LintsInfo")
load("//rust/private:rustc.bzl", "collect_deps", "collect_inputs", "construct_arguments")
load(
"//rust/private:utils.bzl",
"dedent",
"find_cc_toolchain",
"find_toolchain",
"get_lib_name_default",
"get_lib_name_for_windows",
"get_preferred_artifact",
)
def _rustdoc_crate_info(crate_info, output):
"""Clone a `CrateInfo` provider for use by a rustdoc action.
The `CrateInfo` provider documents `output` as a required `File`
([rust/private/providers.bzl](../providers.bzl)) and every other
consumer of `crate_info.output` in the tree assumes it. rustdoc
actions don't produce the crate's compile output (`.rlib`/binary),
so we swap in a rustdoc-owned `File` — the rustdoc HTML directory
when the caller has one, or the crate's root source file as a
valid fallback for actions (like the legacy test-writer path) that
have no rustdoc-produced `File` at analysis time.
Args:
crate_info (CrateInfo): The original provider.
output (File): A `File` to publish as the rustdoc `CrateInfo`'s
`output`. Must be non-`None`.
Returns:
CrateInfo: A modified CrateInfo provider.
"""
return rust_common.create_crate_info(
name = crate_info.name,
type = crate_info.type,
root = crate_info.root,
root_path = crate_info.root_path,
srcs = crate_info.srcs,
deps = crate_info.deps,
proc_macro_deps = crate_info.proc_macro_deps,
aliases = crate_info.aliases,
output = output,
metadata = None,
edition = crate_info.edition,
rustc_env = crate_info.rustc_env,
rustc_env_files = crate_info.rustc_env_files,
is_test = crate_info.is_test,
compile_data = crate_info.compile_data,
compile_data_targets = crate_info.compile_data_targets,
data = crate_info.data,
)
def rustdoc_compile_action(
ctx,
toolchain,
crate_info,
lints_info = None,
output = None,
rustdoc_flags = [],
is_test = False,
force_depend_on_objects = None):
"""Create a struct of information needed for a `rustdoc` compile action based on crate passed to the rustdoc rule.
Args:
ctx (ctx): The rule's context object.
toolchain (rust_toolchain): The currently configured `rust_toolchain`.
crate_info (CrateInfo): The provider of the crate passed to a rustdoc rule.
lints_info (LintsInfo, optional): The LintsInfo provider of the crate passed to the rustdoc rule.
output (File, optional): An optional output a `rustdoc` action is intended to produce.
rustdoc_flags (Args, optional): An `Args` object of `rustdoc` specific flags.
is_test (bool, optional): If True, the action will be configured for `rust_doc_test` targets
force_depend_on_objects (bool, optional): If set, overrides is_test for controlling whether
to depend on .rlib files instead of .rmeta. Defaults to is_test.
Returns:
struct: A struct of some `ctx.actions.run` arguments.
"""
if force_depend_on_objects == None:
force_depend_on_objects = is_test
# Specify rustc flags for lints, if they were provided.
lint_files = []
if lints_info:
rustdoc_flags.add_all(lints_info.rustdoc_lint_flags)
lint_files = lint_files + lints_info.rustdoc_lint_files
# Collect HTML customization files
html_input_files = []
if hasattr(ctx.file, "html_in_header") and ctx.file.html_in_header:
html_input_files.append(ctx.file.html_in_header)
if hasattr(ctx.file, "html_before_content") and ctx.file.html_before_content:
html_input_files.append(ctx.file.html_before_content)
if hasattr(ctx.file, "html_after_content") and ctx.file.html_after_content:
html_input_files.append(ctx.file.html_after_content)
if hasattr(ctx.files, "markdown_css"):
html_input_files.extend(ctx.files.markdown_css)
cc_toolchain, feature_configuration = find_cc_toolchain(ctx)
dep_info, build_info, _ = collect_deps(
deps = crate_info.deps.to_list(),
proc_macro_deps = crate_info.proc_macro_deps.to_list(),
aliases = crate_info.aliases,
)
compile_inputs, out_dir, build_env_files, build_flags_files, linkstamp_outs, ambiguous_libs = collect_inputs(
ctx = ctx,
file = ctx.file,
files = ctx.files,
linkstamps = depset([]),
toolchain = toolchain,
cc_toolchain = cc_toolchain,
feature_configuration = feature_configuration,
crate_info = crate_info,
dep_info = dep_info,
build_info = build_info,
lint_files = lint_files,
force_depend_on_objects = force_depend_on_objects,
include_linker_inputs = is_test or force_depend_on_objects,
include_link_flags = False,
)
# rustdoc actions don't produce the crate's compile output, so we swap in
# a rustdoc-owned `File` for the `CrateInfo` handed to `construct_arguments`.
# Prefer the caller-supplied rustdoc output when available; otherwise fall
# back to the crate root, which is always a `File` per the provider contract.
rustdoc_crate_info = _rustdoc_crate_info(crate_info, output if output != None else crate_info.root)
# rustdoc does not understand linker flags like -lstatic that
# `include_link_flags` generates. So we manually build flags that only apply
# to rustdoc.
if is_test or force_depend_on_objects:
compilation_mode = ctx.var["COMPILATION_MODE"]
use_pic = should_use_pic(
cc_toolchain = cc_toolchain,
feature_configuration = feature_configuration,
crate_type = crate_info.type,
compilation_mode = compilation_mode,
toolchain = toolchain,
)
for_windows = toolchain.target_abi == "msvc"
get_lib_name = get_lib_name_for_windows if for_windows else get_lib_name_default
for dep in dep_info.transitive_noncrates.to_list():
for lib in dep.libraries:
if not (lib.static_library or lib.pic_static_library):
continue
arg = get_lib_name(get_preferred_artifact(lib, use_pic))
if not for_windows:
arg = "-l" + arg
if type(rustdoc_flags) == "Args":
rustdoc_flags.add("-Clink-arg=%s" % arg)
else:
rustdoc_flags.append("-Clink-arg=%s" % arg)
args, env = construct_arguments(
ctx = ctx,
attr = ctx.attr,
file = ctx.file,
toolchain = toolchain,
tool_path = toolchain.rust_doc.short_path if is_test else toolchain.rust_doc.path,
cc_toolchain = cc_toolchain,
feature_configuration = feature_configuration,
crate_info = rustdoc_crate_info,
dep_info = dep_info,
linkstamp_outs = linkstamp_outs,
ambiguous_libs = ambiguous_libs,
output_hash = None,
rust_flags = rustdoc_flags,
out_dir = out_dir,
build_env_files = build_env_files,
build_flags_files = build_flags_files,
emit = [],
remap_path_prefix = None,
add_flags_for_binary = True,
include_link_flags = False,
force_depend_on_objects = force_depend_on_objects,
skip_expanding_rustc_env = True,
)
# Because rustdoc tests compile tests outside of the sandbox, the sysroot
# must be updated to the `short_path` equivalent as it will now be
# a part of runfiles.
if is_test:
if "SYSROOT" in env:
env.update({"SYSROOT": "${{pwd}}/{}".format(toolchain.sysroot_short_path)})
if "OUT_DIR" in env:
env.update({"OUT_DIR": "${{pwd}}/{}".format(build_info.out_dir.short_path)})
# Create the combined inputs including HTML customization files
all_inputs = depset([crate_info.output], transitive = [compile_inputs, depset(html_input_files)])
return struct(
executable = ctx.executable._process_wrapper,
inputs = all_inputs,
env = env,
arguments = args.all,
supports_path_mapping = args.supports_path_mapping,
tools = [toolchain.rust_doc],
)
def _zip_action(ctx, input_dir, output_zip, crate_label):
"""Creates an archive of the generated documentation from `rustdoc`
Args:
ctx (ctx): The `rust_doc` rule's context object
input_dir (File): A directory containing the outputs from rustdoc
output_zip (File): The location of the output archive containing generated documentation
crate_label (Label): The label of the crate docs are being generated for.
"""
args = ctx.actions.args()
args.add(ctx.executable._zipper)
args.add(output_zip)
args.add(ctx.bin_dir.path)
args.add_all([input_dir], expand_directories = True)
ctx.actions.run(
executable = ctx.executable._dir_zipper,
inputs = [input_dir],
outputs = [output_zip],
arguments = [args],
mnemonic = "RustdocZip",
progress_message = "Creating RustdocZip for {}".format(crate_label),
tools = [ctx.executable._zipper],
)
def _rust_doc_impl(ctx):
"""The implementation of the `rust_doc` rule
Args:
ctx (ctx): The rule's context object
"""
if ctx.attr.rustc_flags:
# buildifier: disable=print
print("rustc_flags is deprecated in favor of `rustdoc_flags` for rustdoc targets. Please update {}".format(
ctx.label,
))
crate = ctx.attr.crate
crate_info = crate[rust_common.crate_info]
lints_info = crate[LintsInfo] if LintsInfo in crate else None
output_dir = ctx.actions.declare_directory("{}.rustdoc".format(ctx.label.name))
rustdoc_flags = ctx.actions.args()
rustdoc_flags.add_all(
[crate_info.output],
format_each = "--extern={}=%s".format(crate_info.name),
expand_directories = False,
)
# Add HTML customization flags if attributes are provided
if ctx.attr.html_in_header:
rustdoc_flags.add("--html-in-header", ctx.file.html_in_header)
if ctx.attr.html_before_content:
rustdoc_flags.add("--html-before-content", ctx.file.html_before_content)
if ctx.attr.html_after_content:
rustdoc_flags.add("--html-after-content", ctx.file.html_after_content)
# Add markdown CSS files if provided
for css_file in ctx.files.markdown_css:
rustdoc_flags.add(["--markdown-css", css_file])
rustdoc_flags.add_all(ctx.attr.rustdoc_flags)
action = rustdoc_compile_action(
ctx = ctx,
toolchain = find_toolchain(ctx),
crate_info = crate_info,
lints_info = lints_info,
output = output_dir,
rustdoc_flags = rustdoc_flags,
)
ctx.actions.run(
mnemonic = "Rustdoc",
progress_message = "Generating Rustdoc for {}".format(crate.label),
outputs = [output_dir],
executable = action.executable,
inputs = action.inputs,
env = action.env,
arguments = action.arguments,
tools = action.tools,
toolchain = Label("//rust:toolchain_type"),
)
# This rule does nothing without a single-file output, though the directory should've sufficed.
_zip_action(ctx, output_dir, ctx.outputs.rust_doc_zip, crate.label)
return [
DefaultInfo(
files = depset([output_dir]),
),
OutputGroupInfo(
rustdoc_dir = depset([output_dir]),
rustdoc_zip = depset([ctx.outputs.rust_doc_zip]),
),
]
rust_doc = rule(
doc = dedent("""\
Generates code documentation.
Example:
Suppose you have the following directory structure for a Rust library crate:
```
[workspace]/
WORKSPACE
hello_lib/
BUILD
src/
lib.rs
```
To build [`rustdoc`][rustdoc] documentation for the `hello_lib` crate, define \
a `rust_doc` rule that depends on the the `hello_lib` `rust_library` target:
[rustdoc]: https://doc.rust-lang.org/book/documentation.html
```python
package(default_visibility = ["//visibility:public"])
load("@rules_rust//rust:defs.bzl", "rust_library", "rust_doc")
rust_library(
name = "hello_lib",
srcs = ["src/lib.rs"],
)
rust_doc(
name = "hello_lib_doc",
crate = ":hello_lib",
)
```
Running `bazel build //hello_lib:hello_lib_doc` will build a zip file containing \
the documentation for the `hello_lib` library crate generated by `rustdoc`.
"""),
implementation = _rust_doc_impl,
attrs = {
"crate": attr.label(
doc = (
"The label of the target to generate code documentation for.\n" +
"\n" +
"`rust_doc` can generate HTML code documentation for the source files of " +
"`rust_library` or `rust_binary` targets."
),
providers = [rust_common.crate_info],
mandatory = True,
),
"crate_features": attr.string_list(
doc = dedent("""\
List of features to enable for the crate being documented.
"""),
),
"html_after_content": attr.label(
doc = "File to add in `<body>`, after content.",
allow_single_file = [".html", ".md"],
),
"html_before_content": attr.label(
doc = "File to add in `<body>`, before content.",
allow_single_file = [".html", ".md"],
),
"html_in_header": attr.label(
doc = "File to add to `<head>`.",
allow_single_file = [".html", ".md"],
),
"markdown_css": attr.label_list(
doc = "CSS files to include via `<link>` in a rendered Markdown file.",
allow_files = [".css"],
),
"rustc_flags": attr.string_list(
doc = "**Deprecated**: use `rustdoc_flags` instead",
),
"rustdoc_flags": attr.string_list(
doc = dedent("""\
List of flags passed to `rustdoc`.
These strings are subject to Make variable expansion for predefined
source/output path variables like `$location`, `$execpath`, and
`$rootpath`. This expansion is useful if you wish to pass a generated
file of arguments to rustc: `@$(location //package:target)`.
"""),
),
"_dir_zipper": attr.label(
doc = "A tool that orchestrates the creation of zip archives for rustdoc outputs.",
default = Label("//rust/private/rustdoc/dir_zipper"),
cfg = "exec",
executable = True,
),
"_error_format": attr.label(
default = Label("//rust/settings:error_format"),
),
"_process_wrapper": attr.label(
doc = "A process wrapper for running rustdoc on all platforms",
default = Label("@rules_rust//util/process_wrapper"),
executable = True,
allow_single_file = True,
cfg = "exec",
),
"_zipper": attr.label(
doc = "A Bazel provided tool for creating archives",
default = Label("@bazel_tools//tools/zip:zipper"),
cfg = "exec",
executable = True,
),
},
fragments = ["cpp"],
outputs = {
"rust_doc_zip": "%{name}.zip",
},
toolchains = [
str(Label("//rust:toolchain_type")),
config_common.toolchain_type("@bazel_tools//tools/cpp:toolchain_type", mandatory = False),
],
)