Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 1 | # Copyright 2018 The Bazel Authors. All rights reserved. |
| 2 | # |
| 3 | # Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | # you may not use this file except in compliance with the License. |
| 5 | # You may obtain a copy of the License at |
| 6 | # |
| 7 | # http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | # |
| 9 | # Unless required by applicable law or agreed to in writing, software |
| 10 | # distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | # See the License for the specific language governing permissions and |
| 13 | # limitations under the License. |
| 14 | |
| 15 | """Rules for performing `rustdoc --test` on Bazel built crates""" |
| 16 | |
| 17 | load("//rust/private:common.bzl", "rust_common") |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 18 | load("//rust/private:providers.bzl", "CrateInfo") |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 19 | load("//rust/private:rustdoc.bzl", "rustdoc_compile_action") |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 20 | load("//rust/private:utils.bzl", "dedent", "find_toolchain", "transform_deps") |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 21 | |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 22 | def _construct_writer_arguments(ctx, test_runner, opt_test_params, action, crate_info): |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 23 | """Construct arguments and environment variables specific to `rustdoc_test_writer`. |
| 24 | |
| 25 | This is largely solving for the fact that tests run from a runfiles directory |
| 26 | where actions run in an execroot. But it also tracks what environment variables |
| 27 | were explicitly added to the action. |
| 28 | |
| 29 | Args: |
| 30 | ctx (ctx): The rule's context object. |
| 31 | test_runner (File): The test_runner output file declared by `rustdoc_test`. |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 32 | opt_test_params (File): An output file we can optionally use to store params for `rustdoc`. |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 33 | action (struct): Action arguments generated by `rustdoc_compile_action`. |
| 34 | crate_info (CrateInfo): The provider of the crate who's docs are being tested. |
| 35 | |
| 36 | Returns: |
| 37 | tuple: A tuple of `rustdoc_test_writer` specific inputs |
| 38 | - Args: Arguments for the test writer |
| 39 | - dict: Required environment variables |
| 40 | """ |
| 41 | |
| 42 | writer_args = ctx.actions.args() |
| 43 | |
| 44 | # Track the output path where the test writer should write the test |
| 45 | writer_args.add("--output={}".format(test_runner.path)) |
| 46 | |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 47 | # Track where the test writer should move "spilled" Args to |
| 48 | writer_args.add("--optional_test_params={}".format(opt_test_params.path)) |
| 49 | |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 50 | # Track what environment variables should be written to the test runner |
| 51 | writer_args.add("--action_env=DEVELOPER_DIR") |
| 52 | writer_args.add("--action_env=PATHEXT") |
| 53 | writer_args.add("--action_env=SDKROOT") |
| 54 | writer_args.add("--action_env=SYSROOT") |
| 55 | for var in action.env.keys(): |
| 56 | writer_args.add("--action_env={}".format(var)) |
| 57 | |
| 58 | # Since the test runner will be running from a runfiles directory, the |
| 59 | # paths originally generated for the build action will not map to any |
| 60 | # files. To ensure rustdoc can find the appropriate dependencies, the |
| 61 | # file roots are identified and tracked for each dependency so it can be |
| 62 | # stripped from the test runner. |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 63 | |
| 64 | # Collect and dedupe all of the file roots in a list before appending |
| 65 | # them to args to prevent generating a large amount of identical args |
| 66 | roots = [] |
Adam Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 67 | root = crate_info.output.root.path |
| 68 | if not root in roots: |
| 69 | roots.append(root) |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 70 | for dep in crate_info.deps.to_list(): |
| 71 | dep_crate_info = getattr(dep, "crate_info", None) |
| 72 | dep_dep_info = getattr(dep, "dep_info", None) |
| 73 | if dep_crate_info: |
| 74 | root = dep_crate_info.output.root.path |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 75 | if not root in roots: |
| 76 | roots.append(root) |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 77 | if dep_dep_info: |
| 78 | for direct_dep in dep_dep_info.direct_crates.to_list(): |
| 79 | root = direct_dep.dep.output.root.path |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 80 | if not root in roots: |
| 81 | roots.append(root) |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 82 | for transitive_dep in dep_dep_info.transitive_crates.to_list(): |
| 83 | root = transitive_dep.output.root.path |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 84 | if not root in roots: |
| 85 | roots.append(root) |
| 86 | |
| 87 | for root in roots: |
| 88 | writer_args.add("--strip_substring={}/".format(root)) |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 89 | |
| 90 | # Indicate that the rustdoc_test args are over. |
| 91 | writer_args.add("--") |
| 92 | |
| 93 | # Prepare for the process runner to ingest the rest of the arguments |
| 94 | # to match the expectations of `rustc_compile_action`. |
| 95 | writer_args.add(ctx.executable._process_wrapper.short_path) |
| 96 | |
| 97 | return (writer_args, action.env) |
| 98 | |
| 99 | def _rust_doc_test_impl(ctx): |
| 100 | """The implementation for the `rust_doc_test` rule |
| 101 | |
| 102 | Args: |
| 103 | ctx (ctx): The rule's context object |
| 104 | |
| 105 | Returns: |
| 106 | list: A list containing a DefaultInfo provider |
| 107 | """ |
| 108 | |
| 109 | toolchain = find_toolchain(ctx) |
| 110 | |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 111 | crate = ctx.attr.crate[rust_common.crate_info] |
| 112 | deps = transform_deps(ctx.attr.deps) |
| 113 | |
| 114 | crate_info = rust_common.create_crate_info( |
| 115 | name = crate.name, |
| 116 | type = crate.type, |
| 117 | root = crate.root, |
| 118 | srcs = crate.srcs, |
| 119 | deps = depset(deps, transitive = [crate.deps]), |
| 120 | proc_macro_deps = crate.proc_macro_deps, |
Adam Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 121 | aliases = crate.aliases, |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 122 | output = crate.output, |
| 123 | edition = crate.edition, |
| 124 | rustc_env = crate.rustc_env, |
| 125 | rustc_env_files = crate.rustc_env_files, |
| 126 | is_test = True, |
| 127 | compile_data = crate.compile_data, |
Adam Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 128 | compile_data_targets = crate.compile_data_targets, |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 129 | wrapped_crate_type = crate.type, |
| 130 | owner = ctx.label, |
| 131 | ) |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 132 | |
Adam Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 133 | if toolchain.target_os == "windows": |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 134 | test_runner = ctx.actions.declare_file(ctx.label.name + ".rustdoc_test.bat") |
| 135 | else: |
| 136 | test_runner = ctx.actions.declare_file(ctx.label.name + ".rustdoc_test.sh") |
| 137 | |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 138 | # Bazel will auto-magically spill params to a file, if they are too many for a given OSes shell |
| 139 | # (e.g. Windows ~32k, Linux ~2M). The executable script (aka test_runner) that gets generated, |
| 140 | # is run from the runfiles, which is separate from the params_file Bazel generates. To handle |
| 141 | # this case, we declare our own params file, that the test_writer will populate, if necessary |
| 142 | opt_test_params = ctx.actions.declare_file(ctx.label.name + ".rustdoc_opt_params", sibling = test_runner) |
| 143 | |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 144 | # Add the current crate as an extern for the compile action |
| 145 | rustdoc_flags = [ |
| 146 | "--extern", |
| 147 | "{}={}".format(crate_info.name, crate_info.output.short_path), |
| 148 | "--test", |
| 149 | ] |
| 150 | |
| 151 | action = rustdoc_compile_action( |
| 152 | ctx = ctx, |
| 153 | toolchain = toolchain, |
| 154 | crate_info = crate_info, |
| 155 | rustdoc_flags = rustdoc_flags, |
| 156 | is_test = True, |
| 157 | ) |
| 158 | |
| 159 | tools = action.tools + [ctx.executable._process_wrapper] |
| 160 | |
| 161 | writer_args, env = _construct_writer_arguments( |
| 162 | ctx = ctx, |
| 163 | test_runner = test_runner, |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 164 | opt_test_params = opt_test_params, |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 165 | action = action, |
| 166 | crate_info = crate_info, |
| 167 | ) |
| 168 | |
| 169 | # Allow writer environment variables to override those from the action. |
| 170 | action.env.update(env) |
| 171 | |
| 172 | ctx.actions.run( |
| 173 | mnemonic = "RustdocTestWriter", |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 174 | progress_message = "Generating Rustdoc test runner for {}".format(ctx.attr.crate.label), |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 175 | executable = ctx.executable._test_writer, |
| 176 | inputs = action.inputs, |
| 177 | tools = tools, |
| 178 | arguments = [writer_args] + action.arguments, |
| 179 | env = action.env, |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 180 | outputs = [test_runner, opt_test_params], |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 181 | ) |
| 182 | |
| 183 | return [DefaultInfo( |
| 184 | files = depset([test_runner]), |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 185 | runfiles = ctx.runfiles(files = tools + [opt_test_params], transitive_files = action.inputs), |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 186 | executable = test_runner, |
| 187 | )] |
| 188 | |
| 189 | rust_doc_test = rule( |
| 190 | implementation = _rust_doc_test_impl, |
| 191 | attrs = { |
| 192 | "crate": attr.label( |
| 193 | doc = ( |
| 194 | "The label of the target to generate code documentation for. " + |
| 195 | "`rust_doc_test` can generate HTML code documentation for the " + |
| 196 | "source files of `rust_library` or `rust_binary` targets." |
| 197 | ), |
| 198 | providers = [rust_common.crate_info], |
| 199 | mandatory = True, |
| 200 | ), |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 201 | "deps": attr.label_list( |
| 202 | doc = dedent("""\ |
| 203 | List of other libraries to be linked to this library target. |
| 204 | |
| 205 | These can be either other `rust_library` targets or `cc_library` targets if |
| 206 | linking a native library. |
| 207 | """), |
| 208 | providers = [CrateInfo, CcInfo], |
| 209 | ), |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 210 | "_cc_toolchain": attr.label( |
| 211 | doc = ( |
| 212 | "In order to use find_cc_toolchain, your rule has to depend " + |
| 213 | "on C++ toolchain. See @rules_cc//cc:find_cc_toolchain.bzl " + |
| 214 | "docs for details." |
| 215 | ), |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 216 | default = Label("@bazel_tools//tools/cpp:current_cc_toolchain"), |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 217 | ), |
| 218 | "_process_wrapper": attr.label( |
| 219 | doc = "A process wrapper for running rustdoc on all platforms", |
| 220 | cfg = "exec", |
| 221 | default = Label("//util/process_wrapper"), |
| 222 | executable = True, |
| 223 | ), |
| 224 | "_test_writer": attr.label( |
| 225 | doc = "A binary used for writing script for use as the test executable.", |
| 226 | cfg = "exec", |
| 227 | default = Label("//tools/rustdoc:rustdoc_test_writer"), |
| 228 | executable = True, |
| 229 | ), |
| 230 | }, |
| 231 | test = True, |
| 232 | fragments = ["cpp"], |
| 233 | host_fragments = ["cpp"], |
| 234 | toolchains = [ |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 235 | str(Label("//rust:toolchain_type")), |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 236 | "@bazel_tools//tools/cpp:toolchain_type", |
| 237 | ], |
| 238 | incompatible_use_toolchain_transition = True, |
| 239 | doc = dedent("""\ |
| 240 | Runs Rust documentation tests. |
| 241 | |
| 242 | Example: |
| 243 | |
| 244 | Suppose you have the following directory structure for a Rust library crate: |
| 245 | |
| 246 | ```output |
| 247 | [workspace]/ |
| 248 | WORKSPACE |
| 249 | hello_lib/ |
| 250 | BUILD |
| 251 | src/ |
| 252 | lib.rs |
| 253 | ``` |
| 254 | |
| 255 | To run [documentation tests][doc-test] for the `hello_lib` crate, define a `rust_doc_test` \ |
| 256 | target that depends on the `hello_lib` `rust_library` target: |
| 257 | |
| 258 | [doc-test]: https://doc.rust-lang.org/book/documentation.html#documentation-as-tests |
| 259 | |
| 260 | ```python |
| 261 | package(default_visibility = ["//visibility:public"]) |
| 262 | |
| 263 | load("@rules_rust//rust:defs.bzl", "rust_library", "rust_doc_test") |
| 264 | |
| 265 | rust_library( |
| 266 | name = "hello_lib", |
| 267 | srcs = ["src/lib.rs"], |
| 268 | ) |
| 269 | |
| 270 | rust_doc_test( |
| 271 | name = "hello_lib_doc_test", |
| 272 | crate = ":hello_lib", |
| 273 | ) |
| 274 | ``` |
| 275 | |
| 276 | Running `bazel test //hello_lib:hello_lib_doc_test` will run all documentation tests for the `hello_lib` library crate. |
| 277 | """), |
| 278 | ) |