diff --git a/.bazelci/presubmit.yml b/.bazelci/presubmit.yml
index 3b3990d6a1..c51dedf5bc 100644
--- a/.bazelci/presubmit.yml
+++ b/.bazelci/presubmit.yml
@@ -15,9 +15,6 @@ default_windows_targets: &default_windows_targets
- "-//test/proto/..."
- "-//tools/rust_analyzer/..."
- "-//test/rustfmt/..."
- # rust_doc_test targets are currently broken on windows
- # see: https://github.com/bazelbuild/rules_rust/issues/887
- - "-//test/chained_direct_deps:mod3_doc_test"
tasks:
ubuntu2004:
build_targets: *default_linux_targets
@@ -33,8 +30,6 @@ tasks:
- "//..."
- "//test/..."
- "-//test/conflicting_deps:conflicting_deps_test"
- # rust_doc_test is likely not fully sandboxed
- - "-//test/chained_direct_deps:mod3_doc_test"
macos:
build_targets: *default_macos_targets
test_targets: *default_macos_targets
@@ -64,8 +59,6 @@ tasks:
- "..."
- "//test/..."
- "-//test/conflicting_deps:conflicting_deps_test"
- # rust_doc_test is likely not fully sandboxed
- - "-//test/chained_direct_deps:mod3_doc_test"
build_flags: *aspects_flags
rbe_ubuntu1604_rolling_with_aspects:
name: RBE Rolling Bazel Version With Aspects
@@ -79,8 +72,6 @@ tasks:
- "..."
- "//test/..."
- "-//test/conflicting_deps:conflicting_deps_test"
- # rust_doc_test is likely not fully sandboxed
- - "-//test/chained_direct_deps:mod3_doc_test"
build_flags: *aspects_flags
soft_fail: yes
bazel: "rolling"
@@ -209,9 +200,6 @@ tasks:
- "//..."
# TODO: This requires an updated `rules_foreign_cc`
- "-//sys/..."
- # rust_doc_test is likely not fully sandboxed
- - "-//fibonacci:fibonacci_doc_test"
- - "-//hello_lib:hello_lib_doc_test"
# See https://github.com/bazelbuild/bazel/issues/9987
- "-//ffi/rust_calling_c:matrix_dylib_test"
# The bindgen rules currently do not work on RBE
@@ -255,10 +243,6 @@ tasks:
- "-//proto/..."
# The wasm rules do not work on windows
- "-//wasm/..."
- # rust_doc_test targets are currently broken on windows
- # see: https://github.com/bazelbuild/rules_rust/issues/887
- - "-//hello_lib:hello_lib_doc_test"
- - "-//fibonacci:fibonacci_doc_test"
build_targets: *windows_targets
test_targets: *windows_targets
crate_universe_examples_ubuntu2004:
diff --git a/docs/flatten.md b/docs/flatten.md
index d2fc695f64..305f98ff22 100644
--- a/docs/flatten.md
+++ b/docs/flatten.md
@@ -546,7 +546,7 @@ Running `bazel test //hello_lib:hello_lib_doc_test` will run all documentation t
| Name | Description | Type | Mandatory | Default |
| :------------- | :------------- | :------------- | :------------- | :------------- |
| name | A unique name for this target. | Name | required | |
-| crate | The label of the target to generate code documentation for.
rust_doc_test can generate HTML code documentation for the source files of rust_library or rust_binary targets. | Label | optional | None |
+| crate | The label of the target to generate code documentation for. rust_doc_test can generate HTML code documentation for the source files of rust_library or rust_binary targets. | Label | optional | None |
| dep | __deprecated__: use crate | Label | optional | None |
diff --git a/docs/rust_doc.md b/docs/rust_doc.md
index ac3db5d155..54918932c5 100644
--- a/docs/rust_doc.md
+++ b/docs/rust_doc.md
@@ -115,7 +115,7 @@ Running `bazel test //hello_lib:hello_lib_doc_test` will run all documentation t
| Name | Description | Type | Mandatory | Default |
| :------------- | :------------- | :------------- | :------------- | :------------- |
| name | A unique name for this target. | Name | required | |
-| crate | The label of the target to generate code documentation for.
rust_doc_test can generate HTML code documentation for the source files of rust_library or rust_binary targets. | Label | optional | None |
+| crate | The label of the target to generate code documentation for. rust_doc_test can generate HTML code documentation for the source files of rust_library or rust_binary targets. | Label | optional | None |
| dep | __deprecated__: use crate | Label | optional | None |
diff --git a/rust/private/rustc.bzl b/rust/private/rustc.bzl
index b5cf1d10e0..a53a089e3e 100644
--- a/rust/private/rustc.bzl
+++ b/rust/private/rustc.bzl
@@ -460,7 +460,9 @@ def construct_arguments(
build_flags_files,
emit = ["dep-info", "link"],
force_all_deps_direct = False,
- stamp = False):
+ force_link = False,
+ stamp = False,
+ remap_path_prefix = "."):
"""Builds an Args object containing common rustc flags
Args:
@@ -482,8 +484,10 @@ def construct_arguments(
emit (list): Values for the --emit flag to rustc.
force_all_deps_direct (bool, optional): Whether to pass the transitive rlibs with --extern
to the commandline as opposed to -L.
+ force_link (bool, optional): Whether to add link flags to the command regardless of `emit`.
stamp (bool, optional): Whether or not workspace status stamping is enabled. For more details see
https://docs.bazel.build/versions/main/user-manual.html#flag--stamp
+ remap_path_prefix (str, optional): A value used to remap `${pwd}` to. If set to a falsey value, no prefix will be set.
Returns:
tuple: A tuple of the following items
@@ -576,9 +580,11 @@ def construct_arguments(
rustc_flags.add("--codegen=debuginfo=" + compilation_mode.debug_info)
# For determinism to help with build distribution and such
- rustc_flags.add("--remap-path-prefix=${pwd}=.")
+ if remap_path_prefix:
+ rustc_flags.add("--remap-path-prefix=${{pwd}}={}".format(remap_path_prefix))
- rustc_flags.add("--emit=" + ",".join(emit_with_paths))
+ if emit:
+ rustc_flags.add("--emit=" + ",".join(emit_with_paths))
rustc_flags.add("--color=always")
rustc_flags.add("--target=" + toolchain.target_flag_value)
if hasattr(attr, "crate_features"):
@@ -604,11 +610,14 @@ def construct_arguments(
add_edition_flags(rustc_flags, crate_info)
# Link!
- if "link" in emit:
+ if "link" in emit or force_link:
# Rust's built-in linker can handle linking wasm files. We don't want to attempt to use the cc
# linker since it won't understand.
if toolchain.target_arch != "wasm32":
- rpaths = _compute_rpaths(toolchain, output_dir, dep_info)
+ if output_dir:
+ rpaths = _compute_rpaths(toolchain, output_dir, dep_info)
+ else:
+ rpaths = depset([])
ld, link_args, link_env = get_linker_and_args(ctx, attr, cc_toolchain, feature_configuration, rpaths)
env.update(link_env)
rustc_flags.add("--codegen=linker=" + ld)
diff --git a/rust/private/rustdoc.bzl b/rust/private/rustdoc.bzl
index 7a857f5776..798e135b0e 100644
--- a/rust/private/rustdoc.bzl
+++ b/rust/private/rustdoc.bzl
@@ -15,84 +15,125 @@
"""Rules for generating documentation with `rustdoc` for Bazel built crates"""
load("//rust/private:common.bzl", "rust_common")
-load("//rust/private:rustc.bzl", "add_crate_link_flags", "add_edition_flags")
-load("//rust/private:utils.bzl", "dedent", "find_toolchain")
+load("//rust/private:rustc.bzl", "collect_deps", "collect_inputs", "construct_arguments")
+load("//rust/private:utils.bzl", "dedent", "find_cc_toolchain", "find_toolchain")
-def _rust_doc_impl(ctx):
- """The implementation of the `rust_doc` rule
+def _strip_crate_info_output(crate_info):
+ """Set the CrateInfo.output to None for a given CrateInfo provider.
Args:
- ctx (ctx): The rule's context object
+ crate_info (CrateInfo): A provider
+
+ Returns:
+ CrateInfo: A modified CrateInfo provider
"""
+ return rust_common.create_crate_info(
+ name = crate_info.name,
+ type = crate_info.type,
+ root = crate_info.root,
+ srcs = crate_info.srcs,
+ deps = crate_info.deps,
+ proc_macro_deps = crate_info.proc_macro_deps,
+ aliases = crate_info.aliases,
+ # This crate info should have no output
+ output = None,
+ edition = crate_info.edition,
+ rustc_env = crate_info.rustc_env,
+ is_test = crate_info.is_test,
+ compile_data = crate_info.compile_data,
+ )
- if ctx.attr.crate and ctx.attr.dep:
- fail("{} should only use the `crate` attribute. `dep` is deprecated".format(
- ctx.label,
- ))
+def rustdoc_compile_action(
+ ctx,
+ toolchain,
+ crate_info,
+ output = None,
+ rustdoc_flags = []):
+ """Create a struct of information needed for a `rustdoc` compile action based on crate passed to the rustdoc rule.
- crate = ctx.attr.crate or ctx.attr.dep
- if not crate:
- fail("{} is missing the `crate` attribute".format(ctx.label))
+ 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.
+ output (File, optional): An optional output a `rustdoc` action is intended to produce.
+ rustdoc_flags (list, optional): A list of `rustdoc` specific flags.
- crate_info = crate[rust_common.crate_info]
- dep_info = crate[rust_common.dep_info]
+ Returns:
+ struct: A struct of some `ctx.actions.run` arguments.
+ """
- toolchain = find_toolchain(ctx)
+ # If an output was provided, ensure it's used in rustdoc arguments
+ if output:
+ rustdoc_flags = [
+ "--output",
+ output.path,
+ ] + rustdoc_flags
- rustdoc_inputs = depset(
- [c.output for c in dep_info.transitive_crates.to_list()] +
- [toolchain.rust_doc],
- transitive = [
- crate_info.srcs,
- toolchain.rustc_lib.files,
- toolchain.rust_lib.files,
- ],
+ cc_toolchain, feature_configuration = find_cc_toolchain(ctx)
+
+ dep_info, build_info, linkstamps = collect_deps(
+ label = ctx.label,
+ deps = crate_info.deps,
+ proc_macro_deps = crate_info.proc_macro_deps,
+ aliases = crate_info.aliases,
)
- output_dir = ctx.actions.declare_directory(ctx.label.name)
- args = ctx.actions.args()
- args.add(crate_info.root.path)
- args.add("--crate-name", crate_info.name)
- args.add("--crate-type", crate_info.type)
- if crate_info.type == "proc-macro":
- args.add("--extern")
- args.add("proc_macro")
- args.add("--output", output_dir.path)
- add_edition_flags(args, crate_info)
-
- # nb. rustdoc can't do anything with native link flags; we must omit them.
- add_crate_link_flags(args, dep_info)
-
- args.add_all(ctx.files.markdown_css, before_each = "--markdown-css")
- if ctx.file.html_in_header:
- args.add("--html-in-header", ctx.file.html_in_header)
- if ctx.file.html_before_content:
- args.add("--html-before-content", ctx.file.html_before_content)
- if ctx.file.html_after_content:
- args.add("--html-after-content", ctx.file.html_after_content)
+ compile_inputs, out_dir, build_env_files, build_flags_files, linkstamp_outs = collect_inputs(
+ ctx = ctx,
+ file = ctx.file,
+ files = ctx.files,
+ linkstamps = linkstamps,
+ toolchain = toolchain,
+ cc_toolchain = cc_toolchain,
+ feature_configuration = feature_configuration,
+ crate_info = crate_info,
+ dep_info = dep_info,
+ build_info = build_info,
+ )
- ctx.actions.run(
- executable = toolchain.rust_doc,
- inputs = rustdoc_inputs,
- outputs = [output_dir],
- arguments = [args],
- mnemonic = "Rustdoc",
- progress_message = "Generating rustdoc for {} ({} files)".format(
- crate_info.name,
- len(crate_info.srcs.to_list()),
- ),
+ # Since this crate is not actually producing the output described by the
+ # given CrateInfo, this attribute needs to be stripped to allow the rest
+ # of the rustc functionality in `construct_arguments` to avoid generating
+ # arguments expecting to do so.
+ rustdoc_crate_info = _strip_crate_info_output(crate_info)
+
+ args, env = construct_arguments(
+ ctx = ctx,
+ attr = ctx.attr,
+ file = ctx.file,
+ toolchain = toolchain,
+ tool_path = 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,
+ 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,
+ force_link = True,
)
- # 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)
+ return struct(
+ executable = ctx.executable._process_wrapper,
+ inputs = depset([crate_info.output], transitive = [compile_inputs]),
+ env = env,
+ arguments = args.all,
+ tools = [toolchain.rust_doc],
+ )
-def _zip_action(ctx, input_dir, output_zip):
+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)
@@ -104,9 +145,70 @@ def _zip_action(ctx, input_dir, output_zip):
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.crate and ctx.attr.dep:
+ fail("{} should only use the `crate` attribute. `dep` is deprecated".format(
+ ctx.label,
+ ))
+
+ crate = ctx.attr.crate or ctx.attr.dep
+ if not crate:
+ fail("{} is missing the `crate` attribute".format(ctx.label))
+
+ crate_info = crate[rust_common.crate_info]
+ dep_info = crate[rust_common.dep_info]
+
+ output_dir = ctx.actions.declare_directory("{}.rustdoc".format(ctx.label.name))
+
+ # Add the current crate as an extern for the compile action
+ rustdoc_flags = [
+ "--extern",
+ "{}={}".format(crate_info.name, crate_info.output.path),
+ ]
+
+ action = rustdoc_compile_action(
+ ctx = ctx,
+ toolchain = find_toolchain(ctx),
+ crate_info = crate_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,
+ )
+
+ # 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([ctx.outputs.rust_doc_zip]),
+ ),
+ OutputGroupInfo(
+ rustdoc_dir = depset([output_dir]),
+ rustdoc_zip = depset([ctx.outputs.rust_doc_zip]),
+ ),
+ ]
+
rust_doc = rule(
doc = dedent("""\
Generates code documentation.
@@ -179,17 +281,31 @@ rust_doc = rule(
doc = "CSS files to include via `` in a rendered Markdown file.",
allow_files = [".css"],
),
+ "_cc_toolchain": attr.label(
+ doc = "In order to use find_cpp_toolchain, you must define the '_cc_toolchain' attribute on your rule or aspect.",
+ default = "@bazel_tools//tools/cpp:current_cc_toolchain",
+ ),
"_dir_zipper": attr.label(
+ doc = "A tool that orchestrates the creation of zip archives for rustdoc outputs.",
default = Label("//util/dir_zipper"),
cfg = "exec",
executable = True,
),
+ "_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",
},
diff --git a/rust/private/rustdoc_test.bzl b/rust/private/rustdoc_test.bzl
index 29ca464af7..a6b8ac7a3d 100644
--- a/rust/private/rustdoc_test.bzl
+++ b/rust/private/rustdoc_test.bzl
@@ -15,7 +15,75 @@
"""Rules for performing `rustdoc --test` on Bazel built crates"""
load("//rust/private:common.bzl", "rust_common")
-load("//rust/private:utils.bzl", "dedent", "find_toolchain", "get_lib_name", "get_preferred_artifact")
+load("//rust/private:rustdoc.bzl", "rustdoc_compile_action")
+load("//rust/private:toolchain_utils.bzl", "find_sysroot")
+load("//rust/private:utils.bzl", "dedent", "find_toolchain")
+
+def _construct_writer_arguments(ctx, test_runner, action, crate_info, rust_toolchain):
+ """Construct arguments and environment variables specific to `rustdoc_test_writer`.
+
+ This is largely solving for the fact that tests run from a runfiles directory
+ where actions run in an execroot. But it also tracks what environment variables
+ were explicitly added to the action.
+
+ Args:
+ ctx (ctx): The rule's context object.
+ test_runner (File): The test_runner output file declared by `rustdoc_test`.
+ action (struct): Action arguments generated by `rustdoc_compile_action`.
+ crate_info (CrateInfo): The provider of the crate who's docs are being tested.
+ rust_toolchain (rust_toolchain): The currently configured `rust_toolchain`.
+
+ Returns:
+ tuple: A tuple of `rustdoc_test_writer` specific inputs
+ - Args: Arguments for the test writer
+ - dict: Required environment variables
+ """
+
+ # Set the SYSROOT to the directory of the rust_lib files passed to the toolchain
+ env = {
+ "SYSROOT": "${{pwd}}/{}".format(find_sysroot(rust_toolchain)),
+ }
+
+ writer_args = ctx.actions.args()
+
+ # Track the output path where the test writer should write the test
+ writer_args.add("--output={}".format(test_runner.path))
+
+ # Track what environment variables should be written to the test runner
+ writer_args.add("--action_env=DEVELOPER_DIR")
+ writer_args.add("--action_env=SDKROOT")
+ writer_args.add("--action_env=PATHEXT")
+ writer_args.add("--action_env=SYSROOT")
+ for var in action.env.keys():
+ writer_args.add("--action_env={}".format(var))
+
+ # Since the test runner will be running from a runfiles directory, the
+ # paths originally generated for the build action will not map to any
+ # files. To ensure rustdoc can find the appropriate dependencies, the
+ # file roots are identified and tracked for each dependency so it can be
+ # stripped from the test runner.
+ for dep in crate_info.deps.to_list():
+ dep_crate_info = getattr(dep, "crate_info", None)
+ dep_dep_info = getattr(dep, "dep_info", None)
+ if dep_crate_info:
+ root = dep_crate_info.output.root.path
+ writer_args.add("--strip_substring={}/".format(root))
+ if dep_dep_info:
+ for direct_dep in dep_dep_info.direct_crates.to_list():
+ root = direct_dep.dep.output.root.path
+ writer_args.add("--strip_substring={}/".format(root))
+ for transitive_dep in dep_dep_info.transitive_crates.to_list():
+ root = transitive_dep.output.root.path
+ writer_args.add("--strip_substring={}/".format(root))
+
+ # Indicate that the rustdoc_test args are over.
+ writer_args.add("--")
+
+ # Prepare for the process runner to ingest the rest of the arguments
+ # to match the expectations of `rustc_compile_action`.
+ writer_args.add(ctx.executable._process_wrapper.short_path)
+
+ return (writer_args, action.env)
def _rust_doc_test_impl(ctx):
"""The implementation for the `rust_doc_test` rule
@@ -31,180 +99,70 @@ def _rust_doc_test_impl(ctx):
ctx.label,
))
+ toolchain = find_toolchain(ctx)
+
crate = ctx.attr.crate or ctx.attr.dep
if not crate:
fail("{} is missing the `crate` attribute".format(ctx.label))
-
- toolchain = find_toolchain(ctx)
-
crate_info = crate[rust_common.crate_info]
- dep_info = crate[rust_common.dep_info]
- # Construct rustdoc test command, which will be written to a shell script
- # to be executed to run the test.
- flags = _build_rustdoc_flags(dep_info, crate_info)
- if toolchain.os != "windows":
- rust_doc_test = _build_rustdoc_test_bash_script(ctx, toolchain, flags, crate_info)
+ if toolchain.os == "windows":
+ test_runner = ctx.actions.declare_file(ctx.label.name + ".rustdoc_test.bat")
else:
- rust_doc_test = _build_rustdoc_test_batch_script(ctx, toolchain, flags, crate_info)
-
- # The test script compiles the crate and runs it, so it needs both compile and runtime inputs.
- compile_inputs = depset(
- [crate_info.output] +
- [toolchain.rust_doc] +
- [toolchain.rustc] +
- toolchain.crosstool_files,
- transitive = [
- crate_info.srcs,
- dep_info.transitive_libs,
- toolchain.rustc_lib.files,
- toolchain.rust_lib.files,
- ],
+ test_runner = ctx.actions.declare_file(ctx.label.name + ".rustdoc_test.sh")
+
+ # Add the current crate as an extern for the compile action
+ rustdoc_flags = [
+ "--extern",
+ "{}={}".format(crate_info.name, crate_info.output.short_path),
+ "--test",
+ ]
+
+ action = rustdoc_compile_action(
+ ctx = ctx,
+ toolchain = find_toolchain(ctx),
+ crate_info = crate_info,
+ rustdoc_flags = rustdoc_flags,
)
- return [DefaultInfo(
- runfiles = ctx.runfiles(
- files = compile_inputs.to_list(),
- collect_data = True,
- ),
- executable = rust_doc_test,
- )]
-
-# TODO: Replace with bazel-skylib's `path.dirname`. This requires addressing some dependency issues or
-# generating docs will break.
-def _dirname(path_str):
- """Returns the path of the direcotry from a unix path.
-
- Args:
- path_str (str): A string representing a unix path
-
- Returns:
- str: The parsed directory name of the provided path
- """
- return "/".join(path_str.split("/")[:-1])
-
-def _build_rustdoc_flags(dep_info, crate_info):
- """Constructs the rustdoc script used to test `crate`.
-
- Args:
- dep_info (DepInfo): The DepInfo provider
- crate_info (CrateInfo): The CrateInfo provider
-
- Returns:
- list: A list of rustdoc flags (str)
- """
-
- d = dep_info
-
- # nb. Paths must be constructed wrt runfiles, so we construct relative link flags for doctest.
- link_flags = []
- link_search_flags = []
-
- link_flags.append("--extern=" + crate_info.name + "=" + crate_info.output.short_path)
- link_flags += ["--extern=" + c.name + "=" + c.dep.output.short_path for c in d.direct_crates.to_list()]
- link_search_flags += ["-Ldependency={}".format(_dirname(c.output.short_path)) for c in d.transitive_crates.to_list()]
+ tools = action.tools + [ctx.executable._process_wrapper]
- # TODO(hlopko): use the more robust logic from rustc.bzl also here, through a reasonable API.
- for lib_to_link in dep_info.transitive_noncrates.to_list():
- is_static = bool(lib_to_link.static_library or lib_to_link.pic_static_library)
- f = get_preferred_artifact(lib_to_link)
- if not is_static:
- link_flags.append("-ldylib=" + get_lib_name(f))
- else:
- link_flags.append("-lstatic=" + get_lib_name(f))
- link_flags.append("-Lnative={}".format(_dirname(f.short_path)))
- link_search_flags.append("-Lnative={}".format(_dirname(f.short_path)))
-
- if crate_info.type == "proc-macro":
- link_flags.extend(["--extern", "proc_macro"])
-
- edition_flags = ["--edition={}".format(crate_info.edition)] if crate_info.edition != "2015" else []
-
- return link_search_flags + link_flags + edition_flags
-
-_rustdoc_test_bash_script = """\
-#!/usr/bin/env bash
-
-set -e;
-
-{rust_doc} --test \\
- {crate_root} \\
- --crate-name={crate_name} \\
- {flags}
-"""
-
-def _build_rustdoc_test_bash_script(ctx, toolchain, flags, crate_info):
- """Generates a helper script for executing a rustdoc test for unix systems
-
- Args:
- ctx (ctx): The `rust_doc_test` rule's context object
- toolchain (ToolchainInfo): A rustdoc toolchain
- flags (list): A list of rustdoc flags (str)
- crate_info (CrateInfo): The CrateInfo provider
-
- Returns:
- File: An executable containing information for a rustdoc test
- """
- rust_doc_test = ctx.actions.declare_file(
- ctx.label.name + ".sh",
+ writer_args, env = _construct_writer_arguments(
+ ctx = ctx,
+ test_runner = test_runner,
+ action = action,
+ crate_info = crate_info,
+ rust_toolchain = toolchain,
)
- ctx.actions.write(
- output = rust_doc_test,
- content = _rustdoc_test_bash_script.format(
- rust_doc = toolchain.rust_doc.short_path,
- crate_root = crate_info.root.path,
- crate_name = crate_info.name,
- # TODO: Should be possible to do this with ctx.actions.Args, but can't seem to get them as a str and into the template.
- flags = " \\\n ".join(flags),
- ),
- is_executable = True,
- )
- return rust_doc_test
-
-_rustdoc_test_batch_script = """\
-{rust_doc} --test ^
- {crate_root} ^
- --crate-name={crate_name} ^
- {flags}
-"""
-def _build_rustdoc_test_batch_script(ctx, toolchain, flags, crate_info):
- """Generates a helper script for executing a rustdoc test for windows systems
-
- Args:
- ctx (ctx): The `rust_doc_test` rule's context object
- toolchain (ToolchainInfo): A rustdoc toolchain
- flags (list): A list of rustdoc flags (str)
- crate_info (CrateInfo): The CrateInfo provider
-
- Returns:
- File: An executable containing information for a rustdoc test
- """
- rust_doc_test = ctx.actions.declare_file(
- ctx.label.name + ".bat",
- )
- ctx.actions.write(
- output = rust_doc_test,
- content = _rustdoc_test_batch_script.format(
- rust_doc = toolchain.rust_doc.short_path.replace("/", "\\"),
- crate_root = crate_info.root.path,
- crate_name = crate_info.name,
- # TODO: Should be possible to do this with ctx.actions.Args, but can't seem to get them as a str and into the template.
- flags = " ^\n ".join(flags),
- ),
- is_executable = True,
+ # Allow writer environment variables to override those from the action.
+ action.env.update(env)
+
+ ctx.actions.run(
+ mnemonic = "RustdocTestWriter",
+ progress_message = "Generating Rustdoc test runner for {}".format(crate.label),
+ executable = ctx.executable._test_writer,
+ inputs = action.inputs,
+ tools = tools,
+ arguments = [writer_args] + action.arguments,
+ env = action.env,
+ outputs = [test_runner],
)
- return rust_doc_test
+
+ return [DefaultInfo(
+ files = depset([test_runner]),
+ runfiles = ctx.runfiles(files = tools, transitive_files = action.inputs),
+ executable = test_runner,
+ )]
rust_doc_test = rule(
implementation = _rust_doc_test_impl,
attrs = {
"crate": attr.label(
doc = (
- "The label of the target to generate code documentation for.\n" +
- "\n" +
- "`rust_doc_test` can generate HTML code documentation for the source files of " +
- "`rust_library` or `rust_binary` targets."
+ "The label of the target to generate code documentation for. " +
+ "`rust_doc_test` can generate HTML code documentation for the " +
+ "source files of `rust_library` or `rust_binary` targets."
),
providers = [rust_common.crate_info],
# TODO: Make this attribute mandatory once `dep` is removed
@@ -213,10 +171,34 @@ rust_doc_test = rule(
doc = "__deprecated__: use `crate`",
providers = [rust_common.crate_info],
),
+ "_cc_toolchain": attr.label(
+ doc = (
+ "In order to use find_cc_toolchain, your rule has to depend " +
+ "on C++ toolchain. See @rules_cc//cc:find_cc_toolchain.bzl " +
+ "docs for details."
+ ),
+ default = "@bazel_tools//tools/cpp:current_cc_toolchain",
+ ),
+ "_process_wrapper": attr.label(
+ doc = "A process wrapper for running rustdoc on all platforms",
+ cfg = "exec",
+ default = Label("//util/process_wrapper"),
+ executable = True,
+ ),
+ "_test_writer": attr.label(
+ doc = "A binary used for writing script for use as the test executable.",
+ cfg = "exec",
+ default = Label("//tools/rustdoc:rustdoc_test_writer"),
+ executable = True,
+ ),
},
- executable = True,
test = True,
- toolchains = [str(Label("//rust:toolchain"))],
+ fragments = ["cpp"],
+ host_fragments = ["cpp"],
+ toolchains = [
+ str(Label("//rust:toolchain")),
+ "@bazel_tools//tools/cpp:toolchain_type",
+ ],
incompatible_use_toolchain_transition = True,
doc = dedent("""\
Runs Rust documentation tests.
diff --git a/rust/private/toolchain_utils.bzl b/rust/private/toolchain_utils.bzl
index f26acf298b..aa98a4311b 100644
--- a/rust/private/toolchain_utils.bzl
+++ b/rust/private/toolchain_utils.bzl
@@ -1,5 +1,18 @@
"""A module defining toolchain utilities"""
+def find_sysroot(rust_toolchain):
+ """Locate the rustc sysroot from the `rust_toolchain`
+
+ Args:
+ rust_toolchain (rust_toolchain): The currently configured `rust_toolchain`.
+
+ Returns:
+ str: A path assignable as `SYSROOT` for an action.
+ """
+ sysroot_anchor = rust_toolchain.rust_lib.files.to_list()[0]
+ directory = sysroot_anchor.path.split(sysroot_anchor.short_path, 1)[0]
+ return directory.rstrip("/")
+
def _toolchain_files_impl(ctx):
toolchain = ctx.toolchains[str(Label("//rust:toolchain"))]
diff --git a/tools/rustdoc/BUILD.bazel b/tools/rustdoc/BUILD.bazel
new file mode 100644
index 0000000000..f94175906a
--- /dev/null
+++ b/tools/rustdoc/BUILD.bazel
@@ -0,0 +1,12 @@
+load("//rust:defs.bzl", "rust_binary")
+
+package(default_visibility = ["//visibility:public"])
+
+rust_binary(
+ name = "rustdoc_test_writer",
+ srcs = ["rustdoc_test_writer.rs"],
+ edition = "2018",
+ deps = [
+ "//tools/runfiles",
+ ],
+)
diff --git a/tools/rustdoc/rustdoc_test_writer.rs b/tools/rustdoc/rustdoc_test_writer.rs
new file mode 100644
index 0000000000..33c4c68e61
--- /dev/null
+++ b/tools/rustdoc/rustdoc_test_writer.rs
@@ -0,0 +1,205 @@
+//! A utility for writing scripts for use as test executables intended to match the
+//! subcommands of Bazel build actions so `rustdoc --test`, which builds and tests
+//! code in a single call, can be run as a test target in a hermetic manner.
+
+use std::cmp::Reverse;
+use std::collections::{BTreeSet, HashMap};
+use std::env;
+use std::fs;
+use std::path::{Path, PathBuf};
+
+#[derive(Debug)]
+struct Options {
+ /// A list of environment variable keys to parse from the build action env.
+ env_keys: BTreeSet,
+
+ /// A list of substrings to strip from [Options::action_argv].
+ strip_substrings: Vec,
+
+ /// The path where the script should be written.
+ output: PathBuf,
+
+ /// The `argv` of the configured rustdoc build action.
+ action_argv: Vec,
+}
+
+/// Parse command line arguments
+fn parse_args() -> Options {
+ let args: Vec = env::args().into_iter().collect();
+ let (writer_args, action_args) = {
+ let split = args
+ .iter()
+ .position(|arg| arg == "--")
+ .expect("Unable to find split identifier `--`");
+
+ // Converting each set into a vector makes them easier to parse in
+ // the absence of nightly features
+ let (writer, action) = args.split_at(split);
+ (writer.to_vec(), action.to_vec())
+ };
+
+ // Remove the leading `--` which is expected to be the first
+ // item in `action_args`
+ debug_assert_eq!(action_args[0], "--");
+ let action_argv = action_args[1..].to_vec();
+
+ let output = writer_args
+ .iter()
+ .find(|arg| arg.starts_with("--output="))
+ .and_then(|arg| arg.splitn(2, '=').last())
+ .map(PathBuf::from)
+ .expect("Missing `--output` argument");
+
+ let (strip_substring_args, writer_args): (Vec, Vec) = writer_args
+ .into_iter()
+ .partition(|arg| arg.starts_with("--strip_substring="));
+
+ let mut strip_substrings: Vec = strip_substring_args
+ .into_iter()
+ .map(|arg| {
+ arg.splitn(2, '=')
+ .last()
+ .expect("--strip_substring arguments must have assignments using `=`")
+ .to_owned()
+ })
+ .collect();
+
+ // Strip substrings should always be in reverse order of the length of each
+ // string so when filtering we know that the longer strings are checked
+ // first in order to avoid cases where shorter strings might match longer ones.
+ strip_substrings.sort_by_key(|b| Reverse(b.len()));
+ strip_substrings.dedup();
+
+ let env_keys = writer_args
+ .into_iter()
+ .filter(|arg| arg.starts_with("--action_env="))
+ .map(|arg| {
+ arg.splitn(2, '=')
+ .last()
+ .expect("--env arguments must have assignments using `=`")
+ .to_owned()
+ })
+ .collect();
+
+ Options {
+ env_keys,
+ strip_substrings,
+ output,
+ action_argv,
+ }
+}
+
+/// Write a unix compatible test runner
+fn write_test_runner_unix(
+ path: &Path,
+ env: &HashMap,
+ argv: &[String],
+ strip_substrings: &[String],
+) {
+ let mut content = vec![
+ "#!/usr/bin/env bash".to_owned(),
+ "".to_owned(),
+ "exec env - \\".to_owned(),
+ ];
+
+ content.extend(env.iter().map(|(key, val)| format!("{}='{}' \\", key, val)));
+
+ let argv_str = argv
+ .iter()
+ // Remove any substrings found in the argument
+ .map(|arg| {
+ let mut stripped_arg = arg.to_owned();
+ strip_substrings
+ .iter()
+ .for_each(|substring| stripped_arg = stripped_arg.replace(substring, ""));
+ stripped_arg
+ })
+ .map(|arg| format!("'{}'", arg))
+ .collect::>()
+ .join(" ");
+
+ content.extend(vec![argv_str, "".to_owned()]);
+
+ fs::write(path, content.join("\n")).expect("Failed to write test runner");
+}
+
+/// Write a windows compatible test runner
+fn write_test_runner_windows(
+ path: &Path,
+ env: &HashMap,
+ argv: &[String],
+ strip_substrings: &[String],
+) {
+ let env_str = env
+ .iter()
+ .map(|(key, val)| format!("$env:{}='{}'", key, val))
+ .collect::>()
+ .join(" ; ");
+
+ let argv_str = argv
+ .iter()
+ // Remove any substrings found in the argument
+ .map(|arg| {
+ let mut stripped_arg = arg.to_owned();
+ strip_substrings
+ .iter()
+ .for_each(|substring| stripped_arg = stripped_arg.replace(substring, ""));
+ stripped_arg
+ })
+ .map(|arg| format!("'{}'", arg))
+ .collect::>()
+ .join(" ");
+
+ let content = vec![
+ "@ECHO OFF".to_owned(),
+ "".to_owned(),
+ format!("powershell.exe -c \"{} ; & {}\"", env_str, argv_str),
+ "".to_owned(),
+ ];
+
+ fs::write(path, content.join("\n")).expect("Failed to write test runner");
+}
+
+#[cfg(target_family = "unix")]
+fn set_executable(path: &Path) {
+ use std::os::unix::prelude::PermissionsExt;
+
+ let mut perm = fs::metadata(path)
+ .expect("Failed to get test runner metadata")
+ .permissions();
+
+ perm.set_mode(0o755);
+ fs::set_permissions(path, perm).expect("Failed to set permissions on test runner");
+}
+
+#[cfg(target_family = "windows")]
+fn set_executable(_path: &Path) {
+ // Windows determines whether or not a file is executable via the PATHEXT
+ // environment variable. This function is a no-op for this platform.
+}
+
+fn write_test_runner(
+ path: &Path,
+ env: &HashMap,
+ argv: &[String],
+ strip_substrings: &[String],
+) {
+ if cfg!(target_family = "unix") {
+ write_test_runner_unix(path, env, argv, strip_substrings);
+ } else if cfg!(target_family = "windows") {
+ write_test_runner_windows(path, env, argv, strip_substrings);
+ }
+
+ set_executable(path);
+}
+
+fn main() {
+ let opt = parse_args();
+
+ let env: HashMap = env::vars()
+ .into_iter()
+ .filter(|(key, _)| opt.env_keys.iter().any(|k| k == key))
+ .collect();
+
+ write_test_runner(&opt.output, &env, &opt.action_argv, &opt.strip_substrings);
+}