| # 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 performing `rustdoc --test` on Bazel built crates""" |
| |
| load("//rust/private:common.bzl", "rust_common") |
| load("//rust/private:providers.bzl", "CrateInfo") |
| load("//rust/private:rustdoc.bzl", "rustdoc_compile_action") |
| load("//rust/private:utils.bzl", "dedent", "find_toolchain", "transform_deps") |
| |
| def _construct_writer_arguments(ctx, test_runner, opt_test_params, action, crate_info): |
| """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`. |
| opt_test_params (File): An output file we can optionally use to store params for `rustdoc`. |
| action (struct): Action arguments generated by `rustdoc_compile_action`. |
| crate_info (CrateInfo): The provider of the crate who's docs are being tested. |
| |
| Returns: |
| tuple: A tuple of `rustdoc_test_writer` specific inputs |
| - Args: Arguments for the test writer |
| - dict: Required environment variables |
| """ |
| |
| 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 where the test writer should move "spilled" Args to |
| writer_args.add("--optional_test_params={}".format(opt_test_params.path)) |
| |
| # Track what environment variables should be written to the test runner |
| writer_args.add("--action_env=DEVELOPER_DIR") |
| writer_args.add("--action_env=PATHEXT") |
| writer_args.add("--action_env=SDKROOT") |
| 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. |
| |
| # Collect and dedupe all of the file roots in a list before appending |
| # them to args to prevent generating a large amount of identical args |
| roots = [] |
| root = crate_info.output.root.path |
| if not root in roots: |
| roots.append(root) |
| for dep in crate_info.deps.to_list() + crate_info.proc_macro_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 |
| if not root in roots: |
| roots.append(root) |
| if dep_dep_info: |
| for direct_dep in dep_dep_info.direct_crates.to_list(): |
| root = direct_dep.dep.output.root.path |
| if not root in roots: |
| roots.append(root) |
| for transitive_dep in dep_dep_info.transitive_crates.to_list(): |
| root = transitive_dep.output.root.path |
| if not root in roots: |
| roots.append(root) |
| |
| for root in roots: |
| 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 |
| |
| Args: |
| ctx (ctx): The rule's context object |
| |
| Returns: |
| list: A list containing a DefaultInfo provider |
| """ |
| |
| toolchain = find_toolchain(ctx) |
| |
| crate = ctx.attr.crate[rust_common.crate_info] |
| deps = transform_deps(ctx.attr.deps) |
| |
| crate_info = rust_common.create_crate_info( |
| name = crate.name, |
| type = crate.type, |
| root = crate.root, |
| srcs = crate.srcs, |
| deps = depset(deps, transitive = [crate.deps]), |
| proc_macro_deps = crate.proc_macro_deps, |
| aliases = crate.aliases, |
| output = crate.output, |
| edition = crate.edition, |
| rustc_env = crate.rustc_env, |
| rustc_env_files = crate.rustc_env_files, |
| is_test = True, |
| compile_data = crate.compile_data, |
| compile_data_targets = crate.compile_data_targets, |
| wrapped_crate_type = crate.type, |
| owner = ctx.label, |
| ) |
| |
| if toolchain.target_os == "windows": |
| test_runner = ctx.actions.declare_file(ctx.label.name + ".rustdoc_test.bat") |
| else: |
| test_runner = ctx.actions.declare_file(ctx.label.name + ".rustdoc_test.sh") |
| |
| # Bazel will auto-magically spill params to a file, if they are too many for a given OSes shell |
| # (e.g. Windows ~32k, Linux ~2M). The executable script (aka test_runner) that gets generated, |
| # is run from the runfiles, which is separate from the params_file Bazel generates. To handle |
| # this case, we declare our own params file, that the test_writer will populate, if necessary |
| opt_test_params = ctx.actions.declare_file(ctx.label.name + ".rustdoc_opt_params", sibling = test_runner) |
| |
| # 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 = toolchain, |
| crate_info = crate_info, |
| rustdoc_flags = rustdoc_flags, |
| is_test = True, |
| ) |
| |
| tools = action.tools + [ctx.executable._process_wrapper] |
| |
| writer_args, env = _construct_writer_arguments( |
| ctx = ctx, |
| test_runner = test_runner, |
| opt_test_params = opt_test_params, |
| action = action, |
| crate_info = crate_info, |
| ) |
| |
| # 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(ctx.attr.crate.label), |
| executable = ctx.executable._test_writer, |
| inputs = action.inputs, |
| tools = tools, |
| arguments = [writer_args] + action.arguments, |
| env = action.env, |
| outputs = [test_runner, opt_test_params], |
| ) |
| |
| return [DefaultInfo( |
| files = depset([test_runner]), |
| runfiles = ctx.runfiles(files = tools + [opt_test_params], 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. " + |
| "`rust_doc_test` can generate HTML code documentation for the " + |
| "source files of `rust_library` or `rust_binary` targets." |
| ), |
| providers = [rust_common.crate_info], |
| mandatory = True, |
| ), |
| "deps": attr.label_list( |
| doc = dedent("""\ |
| List of other libraries to be linked to this library target. |
| |
| These can be either other `rust_library` targets or `cc_library` targets if |
| linking a native library. |
| """), |
| providers = [[CrateInfo], [CcInfo]], |
| ), |
| "_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 = Label("@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, |
| ), |
| }, |
| test = True, |
| fragments = ["cpp"], |
| toolchains = [ |
| str(Label("//rust:toolchain_type")), |
| "@bazel_tools//tools/cpp:toolchain_type", |
| ], |
| doc = dedent("""\ |
| Runs Rust documentation tests. |
| |
| Example: |
| |
| Suppose you have the following directory structure for a Rust library crate: |
| |
| ```output |
| [workspace]/ |
| WORKSPACE |
| hello_lib/ |
| BUILD |
| src/ |
| lib.rs |
| ``` |
| |
| To run [documentation tests][doc-test] for the `hello_lib` crate, define a `rust_doc_test` \ |
| target that depends on the `hello_lib` `rust_library` target: |
| |
| [doc-test]: https://doc.rust-lang.org/book/documentation.html#documentation-as-tests |
| |
| ```python |
| package(default_visibility = ["//visibility:public"]) |
| |
| load("@rules_rust//rust:defs.bzl", "rust_library", "rust_doc_test") |
| |
| rust_library( |
| name = "hello_lib", |
| srcs = ["src/lib.rs"], |
| ) |
| |
| rust_doc_test( |
| name = "hello_lib_doc_test", |
| crate = ":hello_lib", |
| ) |
| ``` |
| |
| Running `bazel test //hello_lib:hello_lib_doc_test` will run all documentation tests for the `hello_lib` library crate. |
| """), |
| ) |