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 17load("//rust/private:common.bzl", "rust_common") 18load("//rust/private:providers.bzl", "CrateInfo") 19load("//rust/private:rustdoc.bzl", "rustdoc_compile_action") 20load("//rust/private:utils.bzl", "dedent", "find_toolchain", "transform_deps") 21 22def _construct_writer_arguments(ctx, test_runner, opt_test_params, action, crate_info): 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`. 32 opt_test_params (File): An output file we can optionally use to store params for `rustdoc`. 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 47 # Track where the test writer should move "spilled" Args to 48 writer_args.add("--optional_test_params={}".format(opt_test_params.path)) 49 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. 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 = [] 67 root = crate_info.output.root.path 68 if not root in roots: 69 roots.append(root) 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 75 if not root in roots: 76 roots.append(root) 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 80 if not root in roots: 81 roots.append(root) 82 for transitive_dep in dep_dep_info.transitive_crates.to_list(): 83 root = transitive_dep.output.root.path 84 if not root in roots: 85 roots.append(root) 86 87 for root in roots: 88 writer_args.add("--strip_substring={}/".format(root)) 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 99def _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 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, 121 aliases = crate.aliases, 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, 128 compile_data_targets = crate.compile_data_targets, 129 wrapped_crate_type = crate.type, 130 owner = ctx.label, 131 ) 132 133 if toolchain.target_os == "windows": 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 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 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, 164 opt_test_params = opt_test_params, 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", 174 progress_message = "Generating Rustdoc test runner for {}".format(ctx.attr.crate.label), 175 executable = ctx.executable._test_writer, 176 inputs = action.inputs, 177 tools = tools, 178 arguments = [writer_args] + action.arguments, 179 env = action.env, 180 outputs = [test_runner, opt_test_params], 181 ) 182 183 return [DefaultInfo( 184 files = depset([test_runner]), 185 runfiles = ctx.runfiles(files = tools + [opt_test_params], transitive_files = action.inputs), 186 executable = test_runner, 187 )] 188 189rust_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 ), 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 ), 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 ), 216 default = Label("@bazel_tools//tools/cpp:current_cc_toolchain"), 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 toolchains = [ 234 str(Label("//rust:toolchain_type")), 235 "@bazel_tools//tools/cpp:toolchain_type", 236 ], 237 doc = dedent("""\ 238 Runs Rust documentation tests. 239 240 Example: 241 242 Suppose you have the following directory structure for a Rust library crate: 243 244 ```output 245 [workspace]/ 246 WORKSPACE 247 hello_lib/ 248 BUILD 249 src/ 250 lib.rs 251 ``` 252 253 To run [documentation tests][doc-test] for the `hello_lib` crate, define a `rust_doc_test` \ 254 target that depends on the `hello_lib` `rust_library` target: 255 256 [doc-test]: https://doc.rust-lang.org/book/documentation.html#documentation-as-tests 257 258 ```python 259 package(default_visibility = ["//visibility:public"]) 260 261 load("@rules_rust//rust:defs.bzl", "rust_library", "rust_doc_test") 262 263 rust_library( 264 name = "hello_lib", 265 srcs = ["src/lib.rs"], 266 ) 267 268 rust_doc_test( 269 name = "hello_lib_doc_test", 270 crate = ":hello_lib", 271 ) 272 ``` 273 274 Running `bazel test //hello_lib:hello_lib_doc_test` will run all documentation tests for the `hello_lib` library crate. 275 """), 276) 277