blob: 9fb73e1eff1ac9bb3ebdc0eebc49b4aedac5a533 [file] [log] [blame]
Brian Silvermancc09f182022-03-09 15:40:20 -08001# 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")
Brian Silverman5f6f2762022-08-13 19:30:05 -070018load("//rust/private:providers.bzl", "CrateInfo")
Brian Silvermancc09f182022-03-09 15:40:20 -080019load("//rust/private:rustdoc.bzl", "rustdoc_compile_action")
Brian Silverman5f6f2762022-08-13 19:30:05 -070020load("//rust/private:utils.bzl", "dedent", "find_toolchain", "transform_deps")
Brian Silvermancc09f182022-03-09 15:40:20 -080021
Brian Silverman5f6f2762022-08-13 19:30:05 -070022def _construct_writer_arguments(ctx, test_runner, opt_test_params, action, crate_info):
Brian Silvermancc09f182022-03-09 15:40:20 -080023 """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 Silverman5f6f2762022-08-13 19:30:05 -070032 opt_test_params (File): An output file we can optionally use to store params for `rustdoc`.
Brian Silvermancc09f182022-03-09 15:40:20 -080033 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 Silverman5f6f2762022-08-13 19:30:05 -070047 # Track where the test writer should move "spilled" Args to
48 writer_args.add("--optional_test_params={}".format(opt_test_params.path))
49
Brian Silvermancc09f182022-03-09 15:40:20 -080050 # 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 Silverman5f6f2762022-08-13 19:30:05 -070063
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 Snaider1c095c92023-07-08 02:09:58 -040067 root = crate_info.output.root.path
68 if not root in roots:
69 roots.append(root)
Brian Silvermancc09f182022-03-09 15:40:20 -080070 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 Silverman5f6f2762022-08-13 19:30:05 -070075 if not root in roots:
76 roots.append(root)
Brian Silvermancc09f182022-03-09 15:40:20 -080077 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 Silverman5f6f2762022-08-13 19:30:05 -070080 if not root in roots:
81 roots.append(root)
Brian Silvermancc09f182022-03-09 15:40:20 -080082 for transitive_dep in dep_dep_info.transitive_crates.to_list():
83 root = transitive_dep.output.root.path
Brian Silverman5f6f2762022-08-13 19:30:05 -070084 if not root in roots:
85 roots.append(root)
86
87 for root in roots:
88 writer_args.add("--strip_substring={}/".format(root))
Brian Silvermancc09f182022-03-09 15:40:20 -080089
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
Brian Silverman5f6f2762022-08-13 19:30:05 -0700111 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 Snaider1c095c92023-07-08 02:09:58 -0400121 aliases = crate.aliases,
Brian Silverman5f6f2762022-08-13 19:30:05 -0700122 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 Snaider1c095c92023-07-08 02:09:58 -0400128 compile_data_targets = crate.compile_data_targets,
Brian Silverman5f6f2762022-08-13 19:30:05 -0700129 wrapped_crate_type = crate.type,
130 owner = ctx.label,
131 )
Brian Silvermancc09f182022-03-09 15:40:20 -0800132
Adam Snaider1c095c92023-07-08 02:09:58 -0400133 if toolchain.target_os == "windows":
Brian Silvermancc09f182022-03-09 15:40:20 -0800134 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 Silverman5f6f2762022-08-13 19:30:05 -0700138 # 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 Silvermancc09f182022-03-09 15:40:20 -0800144 # 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 Silverman5f6f2762022-08-13 19:30:05 -0700164 opt_test_params = opt_test_params,
Brian Silvermancc09f182022-03-09 15:40:20 -0800165 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 Silverman5f6f2762022-08-13 19:30:05 -0700174 progress_message = "Generating Rustdoc test runner for {}".format(ctx.attr.crate.label),
Brian Silvermancc09f182022-03-09 15:40:20 -0800175 executable = ctx.executable._test_writer,
176 inputs = action.inputs,
177 tools = tools,
178 arguments = [writer_args] + action.arguments,
179 env = action.env,
Brian Silverman5f6f2762022-08-13 19:30:05 -0700180 outputs = [test_runner, opt_test_params],
Brian Silvermancc09f182022-03-09 15:40:20 -0800181 )
182
183 return [DefaultInfo(
184 files = depset([test_runner]),
Brian Silverman5f6f2762022-08-13 19:30:05 -0700185 runfiles = ctx.runfiles(files = tools + [opt_test_params], transitive_files = action.inputs),
Brian Silvermancc09f182022-03-09 15:40:20 -0800186 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 ),
Brian Silverman5f6f2762022-08-13 19:30:05 -0700201 "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 Silvermancc09f182022-03-09 15:40:20 -0800210 "_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 Silverman5f6f2762022-08-13 19:30:05 -0700216 default = Label("@bazel_tools//tools/cpp:current_cc_toolchain"),
Brian Silvermancc09f182022-03-09 15:40:20 -0800217 ),
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 Silverman5f6f2762022-08-13 19:30:05 -0700235 str(Label("//rust:toolchain_type")),
Brian Silvermancc09f182022-03-09 15:40:20 -0800236 "@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)