blob: a7e710013dbf8b973f9d556d8bfdc8756272cdd4 [file] [log] [blame]
Brian Silvermancc09f182022-03-09 15:40:20 -08001"""A module defining rustfmt rules"""
2
3load(":common.bzl", "rust_common")
Brian Silvermancc09f182022-03-09 15:40:20 -08004
Adam Snaider1c095c92023-07-08 02:09:58 -04005def _get_rustfmt_ready_crate_info(target):
6 """Check that a target is suitable for rustfmt and extract the `CrateInfo` provider from it.
Brian Silvermancc09f182022-03-09 15:40:20 -08007
8 Args:
9 target (Target): The target the aspect is running on.
Adam Snaider1c095c92023-07-08 02:09:58 -040010
11 Returns:
12 CrateInfo, optional: A `CrateInfo` provider if clippy should be run or `None`.
13 """
14
15 # Ignore external targets
16 if target.label.workspace_root.startswith("external"):
17 return None
18
19 # Obviously ignore any targets that don't contain `CrateInfo`
20 if rust_common.crate_info in target:
21 return target[rust_common.crate_info]
22 elif rust_common.test_crate_info in target:
23 return target[rust_common.test_crate_info].crate
24 else:
25 return None
26
27def _find_rustfmtable_srcs(crate_info, aspect_ctx = None):
28 """Parse a `CrateInfo` provider for rustfmt formattable sources.
29
30 Args:
31 crate_info (CrateInfo): A `CrateInfo` provider.
Brian Silvermancc09f182022-03-09 15:40:20 -080032 aspect_ctx (ctx, optional): The aspect's context object.
33
34 Returns:
35 list: A list of formattable sources (`File`).
36 """
Brian Silvermancc09f182022-03-09 15:40:20 -080037
Adam Snaider1c095c92023-07-08 02:09:58 -040038 # Targets with specific tags will not be formatted
Brian Silverman5f6f2762022-08-13 19:30:05 -070039 if aspect_ctx:
Brian Silverman5f6f2762022-08-13 19:30:05 -070040 ignore_tags = [
41 "no-format",
42 "no-rustfmt",
43 "norustfmt",
44 ]
45
46 for tag in ignore_tags:
47 if tag in aspect_ctx.rule.attr.tags:
48 return []
Brian Silvermancc09f182022-03-09 15:40:20 -080049
Brian Silvermancc09f182022-03-09 15:40:20 -080050 # Filter out any generated files
51 srcs = [src for src in crate_info.srcs.to_list() if src.is_source]
52
53 return srcs
54
55def _generate_manifest(edition, srcs, ctx):
56 # Gather the source paths to non-generated files
57 src_paths = [src.path for src in srcs]
58
59 # Write the rustfmt manifest
60 manifest = ctx.actions.declare_file(ctx.label.name + ".rustfmt")
61 ctx.actions.write(
62 output = manifest,
63 content = "\n".join(src_paths + [
64 edition,
65 ]),
66 )
67
68 return manifest
69
70def _perform_check(edition, srcs, ctx):
Adam Snaider1c095c92023-07-08 02:09:58 -040071 rustfmt_toolchain = ctx.toolchains[Label("//rust/rustfmt:toolchain_type")]
72
Brian Silvermancc09f182022-03-09 15:40:20 -080073 config = ctx.file._config
74 marker = ctx.actions.declare_file(ctx.label.name + ".rustfmt.ok")
75
76 args = ctx.actions.args()
77 args.add("--touch-file")
78 args.add(marker)
79 args.add("--")
Adam Snaider1c095c92023-07-08 02:09:58 -040080 args.add(rustfmt_toolchain.rustfmt)
Brian Silvermancc09f182022-03-09 15:40:20 -080081 args.add("--config-path")
82 args.add(config)
83 args.add("--edition")
84 args.add(edition)
85 args.add("--check")
86 args.add_all(srcs)
87
88 ctx.actions.run(
89 executable = ctx.executable._process_wrapper,
90 inputs = srcs + [config],
91 outputs = [marker],
Adam Snaider1c095c92023-07-08 02:09:58 -040092 tools = [rustfmt_toolchain.all_files],
Brian Silvermancc09f182022-03-09 15:40:20 -080093 arguments = [args],
94 mnemonic = "Rustfmt",
95 )
96
97 return marker
98
99def _rustfmt_aspect_impl(target, ctx):
Adam Snaider1c095c92023-07-08 02:09:58 -0400100 crate_info = _get_rustfmt_ready_crate_info(target)
101
102 if not crate_info:
103 return []
104
105 srcs = _find_rustfmtable_srcs(crate_info, ctx)
Brian Silvermancc09f182022-03-09 15:40:20 -0800106
107 # If there are no formattable sources, do nothing.
108 if not srcs:
109 return []
110
Adam Snaider1c095c92023-07-08 02:09:58 -0400111 edition = crate_info.edition
Brian Silvermancc09f182022-03-09 15:40:20 -0800112
Brian Silvermancc09f182022-03-09 15:40:20 -0800113 marker = _perform_check(edition, srcs, ctx)
114
115 return [
116 OutputGroupInfo(
Brian Silvermancc09f182022-03-09 15:40:20 -0800117 rustfmt_checks = depset([marker]),
118 ),
119 ]
120
121rustfmt_aspect = aspect(
122 implementation = _rustfmt_aspect_impl,
123 doc = """\
124This aspect is used to gather information about a crate for use in rustfmt and perform rustfmt checks
125
126Output Groups:
127
Brian Silvermancc09f182022-03-09 15:40:20 -0800128- `rustfmt_checks`: Executes `rustfmt --check` on the specified target.
129
130The build setting `@rules_rust//:rustfmt.toml` is used to control the Rustfmt [configuration settings][cs]
131used at runtime.
132
133[cs]: https://rust-lang.github.io/rustfmt/
134
135This aspect is executed on any target which provides the `CrateInfo` provider. However
Brian Silverman5f6f2762022-08-13 19:30:05 -0700136users may tag a target with `no-rustfmt` or `no-format` to have it skipped. Additionally,
137generated source files are also ignored by this aspect.
Brian Silvermancc09f182022-03-09 15:40:20 -0800138""",
139 attrs = {
140 "_config": attr.label(
141 doc = "The `rustfmt.toml` file used for formatting",
142 allow_single_file = True,
143 default = Label("//:rustfmt.toml"),
144 ),
145 "_process_wrapper": attr.label(
146 doc = "A process wrapper for running rustfmt on all platforms",
147 cfg = "exec",
148 executable = True,
149 default = Label("//util/process_wrapper"),
150 ),
151 },
152 incompatible_use_toolchain_transition = True,
Adam Snaider1c095c92023-07-08 02:09:58 -0400153 required_providers = [
154 [rust_common.crate_info],
155 [rust_common.test_crate_info],
156 ],
Brian Silvermancc09f182022-03-09 15:40:20 -0800157 fragments = ["cpp"],
158 host_fragments = ["cpp"],
159 toolchains = [
Adam Snaider1c095c92023-07-08 02:09:58 -0400160 str(Label("//rust/rustfmt:toolchain_type")),
161 ],
162)
163
164def _rustfmt_test_manifest_aspect_impl(target, ctx):
165 crate_info = _get_rustfmt_ready_crate_info(target)
166
167 if not crate_info:
168 return []
169
170 # Parse the edition to use for formatting from the target
171 edition = crate_info.edition
172
173 srcs = _find_rustfmtable_srcs(crate_info, ctx)
174 manifest = _generate_manifest(edition, srcs, ctx)
175
176 return [
177 OutputGroupInfo(
178 rustfmt_manifest = depset([manifest]),
179 ),
180 ]
181
182# This aspect contains functionality split out of `rustfmt_aspect` which broke when
183# `required_providers` was added to it. Aspects which have `required_providers` seems
184# to not function with attributes that also require providers.
185_rustfmt_test_manifest_aspect = aspect(
186 implementation = _rustfmt_test_manifest_aspect_impl,
187 doc = """\
188This aspect is used to gather information about a crate for use in `rustfmt_test`
189
190Output Groups:
191
192- `rustfmt_manifest`: A manifest used by rustfmt binaries to provide crate specific settings.
193""",
194 incompatible_use_toolchain_transition = True,
195 fragments = ["cpp"],
196 host_fragments = ["cpp"],
197 toolchains = [
198 str(Label("//rust/rustfmt:toolchain_type")),
Brian Silvermancc09f182022-03-09 15:40:20 -0800199 ],
200)
201
202def _rustfmt_test_impl(ctx):
203 # The executable of a test target must be the output of an action in
204 # the rule implementation. This file is simply a symlink to the real
205 # rustfmt test runner.
Brian Silverman5f6f2762022-08-13 19:30:05 -0700206 is_windows = ctx.executable._runner.extension == ".exe"
Brian Silvermancc09f182022-03-09 15:40:20 -0800207 runner = ctx.actions.declare_file("{}{}".format(
208 ctx.label.name,
Brian Silverman5f6f2762022-08-13 19:30:05 -0700209 ".exe" if is_windows else "",
Brian Silvermancc09f182022-03-09 15:40:20 -0800210 ))
211
212 ctx.actions.symlink(
213 output = runner,
214 target_file = ctx.executable._runner,
215 is_executable = True,
216 )
217
Adam Snaider1c095c92023-07-08 02:09:58 -0400218 crate_infos = [_get_rustfmt_ready_crate_info(target) for target in ctx.attr.targets]
219 srcs = [depset(_find_rustfmtable_srcs(crate_info)) for crate_info in crate_infos if crate_info]
220
221 # Some targets may be included in tests but tagged as "no-format". In this
222 # case, there will be no manifest.
223 manifests = [getattr(target[OutputGroupInfo], "rustfmt_manifest", None) for target in ctx.attr.targets]
224 manifests = depset(transitive = [manifest for manifest in manifests if manifest])
Brian Silvermancc09f182022-03-09 15:40:20 -0800225
226 runfiles = ctx.runfiles(
Brian Silverman5f6f2762022-08-13 19:30:05 -0700227 transitive_files = depset(transitive = srcs + [manifests]),
Brian Silvermancc09f182022-03-09 15:40:20 -0800228 )
229
230 runfiles = runfiles.merge(
231 ctx.attr._runner[DefaultInfo].default_runfiles,
232 )
233
Brian Silverman5f6f2762022-08-13 19:30:05 -0700234 path_env_sep = ";" if is_windows else ":"
235
236 return [
237 DefaultInfo(
238 files = depset([runner]),
239 runfiles = runfiles,
240 executable = runner,
241 ),
242 testing.TestEnvironment({
243 "RUSTFMT_MANIFESTS": path_env_sep.join([
244 manifest.short_path
245 for manifest in sorted(manifests.to_list())
246 ]),
247 "RUST_BACKTRACE": "1",
248 }),
249 ]
Brian Silvermancc09f182022-03-09 15:40:20 -0800250
251rustfmt_test = rule(
252 implementation = _rustfmt_test_impl,
253 doc = "A test rule for performing `rustfmt --check` on a set of targets",
254 attrs = {
255 "targets": attr.label_list(
256 doc = "Rust targets to run `rustfmt --check` on.",
Adam Snaider1c095c92023-07-08 02:09:58 -0400257 providers = [
258 [rust_common.crate_info],
259 [rust_common.test_crate_info],
260 ],
261 aspects = [_rustfmt_test_manifest_aspect],
Brian Silvermancc09f182022-03-09 15:40:20 -0800262 ),
263 "_runner": attr.label(
264 doc = "The rustfmt test runner",
265 cfg = "exec",
266 executable = True,
267 default = Label("//tools/rustfmt:rustfmt_test"),
268 ),
269 },
270 test = True,
271)
Brian Silverman5f6f2762022-08-13 19:30:05 -0700272
Adam Snaider1c095c92023-07-08 02:09:58 -0400273def _rustfmt_toolchain_impl(ctx):
274 make_variables = {
275 "RUSTFMT": ctx.file.rustfmt.path,
276 }
Brian Silverman5f6f2762022-08-13 19:30:05 -0700277
Adam Snaider1c095c92023-07-08 02:09:58 -0400278 if ctx.attr.rustc:
279 make_variables.update({
280 "RUSTC": ctx.file.rustc.path,
281 })
282
283 make_variable_info = platform_common.TemplateVariableInfo(make_variables)
284
285 all_files = [ctx.file.rustfmt] + ctx.files.rustc_lib
286 if ctx.file.rustc:
287 all_files.append(ctx.file.rustc)
288
289 toolchain = platform_common.ToolchainInfo(
290 rustfmt = ctx.file.rustfmt,
291 rustc = ctx.file.rustc,
292 rustc_lib = depset(ctx.files.rustc_lib),
293 all_files = depset(all_files),
294 make_variables = make_variable_info,
Brian Silverman5f6f2762022-08-13 19:30:05 -0700295 )
296
Adam Snaider1c095c92023-07-08 02:09:58 -0400297 return [
298 toolchain,
299 make_variable_info,
300 ]
Brian Silverman5f6f2762022-08-13 19:30:05 -0700301
Adam Snaider1c095c92023-07-08 02:09:58 -0400302rustfmt_toolchain = rule(
303 doc = "A toolchain for [rustfmt](https://rust-lang.github.io/rustfmt/)",
304 implementation = _rustfmt_toolchain_impl,
305 incompatible_use_toolchain_transition = True,
306 attrs = {
307 "rustc": attr.label(
308 doc = "The location of the `rustc` binary. Can be a direct source or a filegroup containing one item.",
309 allow_single_file = True,
310 cfg = "exec",
311 ),
312 "rustc_lib": attr.label(
313 doc = "The libraries used by rustc during compilation.",
314 cfg = "exec",
315 ),
316 "rustfmt": attr.label(
317 doc = "The location of the `rustfmt` binary. Can be a direct source or a filegroup containing one item.",
318 allow_single_file = True,
319 cfg = "exec",
320 mandatory = True,
321 ),
322 },
323 toolchains = [
324 str(Label("@rules_rust//rust:toolchain_type")),
325 ],
326)
327
328def _current_rustfmt_toolchain_impl(ctx):
329 toolchain = ctx.toolchains[str(Label("@rules_rust//rust/rustfmt:toolchain_type"))]
330
331 return [
332 toolchain,
333 toolchain.make_variables,
334 DefaultInfo(
335 files = depset([
336 toolchain.rustfmt,
337 ]),
338 runfiles = ctx.runfiles(transitive_files = toolchain.all_files),
339 ),
340 ]
341
342current_rustfmt_toolchain = rule(
343 doc = "A rule for exposing the current registered `rustfmt_toolchain`.",
344 implementation = _current_rustfmt_toolchain_impl,
345 toolchains = [
346 str(Label("@rules_rust//rust/rustfmt:toolchain_type")),
347 ],
348 incompatible_use_toolchain_transition = True,
Brian Silverman5f6f2762022-08-13 19:30:05 -0700349)