blob: 9c59f4886273d8ad0c1577da56037ef8d1c50a4c [file] [log] [blame]
Brian Silvermancc09f182022-03-09 15:40:20 -08001"""A module defining rustfmt rules"""
2
3load(":common.bzl", "rust_common")
4load(":utils.bzl", "find_toolchain")
5
6def _find_rustfmtable_srcs(target, aspect_ctx = None):
7 """Parse a target for rustfmt formattable sources.
8
9 Args:
10 target (Target): The target the aspect is running on.
11 aspect_ctx (ctx, optional): The aspect's context object.
12
13 Returns:
14 list: A list of formattable sources (`File`).
15 """
16 if rust_common.crate_info not in target:
17 return []
18
19 # Ignore external targets
20 if target.label.workspace_root.startswith("external"):
21 return []
22
23 # Targets annotated with `norustfmt` will not be formatted
24 if aspect_ctx and "norustfmt" in aspect_ctx.rule.attr.tags:
25 return []
26
27 crate_info = target[rust_common.crate_info]
28
29 # Filter out any generated files
30 srcs = [src for src in crate_info.srcs.to_list() if src.is_source]
31
32 return srcs
33
34def _generate_manifest(edition, srcs, ctx):
35 # Gather the source paths to non-generated files
36 src_paths = [src.path for src in srcs]
37
38 # Write the rustfmt manifest
39 manifest = ctx.actions.declare_file(ctx.label.name + ".rustfmt")
40 ctx.actions.write(
41 output = manifest,
42 content = "\n".join(src_paths + [
43 edition,
44 ]),
45 )
46
47 return manifest
48
49def _perform_check(edition, srcs, ctx):
50 toolchain = find_toolchain(ctx)
51 config = ctx.file._config
52 marker = ctx.actions.declare_file(ctx.label.name + ".rustfmt.ok")
53
54 args = ctx.actions.args()
55 args.add("--touch-file")
56 args.add(marker)
57 args.add("--")
58 args.add(toolchain.rustfmt)
59 args.add("--config-path")
60 args.add(config)
61 args.add("--edition")
62 args.add(edition)
63 args.add("--check")
64 args.add_all(srcs)
65
66 ctx.actions.run(
67 executable = ctx.executable._process_wrapper,
68 inputs = srcs + [config],
69 outputs = [marker],
70 tools = [toolchain.rustfmt],
71 arguments = [args],
72 mnemonic = "Rustfmt",
73 )
74
75 return marker
76
77def _rustfmt_aspect_impl(target, ctx):
78 srcs = _find_rustfmtable_srcs(target, ctx)
79
80 # If there are no formattable sources, do nothing.
81 if not srcs:
82 return []
83
84 # Parse the edition to use for formatting from the target
85 edition = target[rust_common.crate_info].edition
86
87 manifest = _generate_manifest(edition, srcs, ctx)
88 marker = _perform_check(edition, srcs, ctx)
89
90 return [
91 OutputGroupInfo(
92 rustfmt_manifest = depset([manifest]),
93 rustfmt_checks = depset([marker]),
94 ),
95 ]
96
97rustfmt_aspect = aspect(
98 implementation = _rustfmt_aspect_impl,
99 doc = """\
100This aspect is used to gather information about a crate for use in rustfmt and perform rustfmt checks
101
102Output Groups:
103
104- `rustfmt_manifest`: A manifest used by rustfmt binaries to provide crate specific settings.
105- `rustfmt_checks`: Executes `rustfmt --check` on the specified target.
106
107The build setting `@rules_rust//:rustfmt.toml` is used to control the Rustfmt [configuration settings][cs]
108used at runtime.
109
110[cs]: https://rust-lang.github.io/rustfmt/
111
112This aspect is executed on any target which provides the `CrateInfo` provider. However
113users may tag a target with `norustfmt` to have it skipped. Additionally, generated
114source files are also ignored by this aspect.
115""",
116 attrs = {
117 "_config": attr.label(
118 doc = "The `rustfmt.toml` file used for formatting",
119 allow_single_file = True,
120 default = Label("//:rustfmt.toml"),
121 ),
122 "_process_wrapper": attr.label(
123 doc = "A process wrapper for running rustfmt on all platforms",
124 cfg = "exec",
125 executable = True,
126 default = Label("//util/process_wrapper"),
127 ),
128 },
129 incompatible_use_toolchain_transition = True,
130 fragments = ["cpp"],
131 host_fragments = ["cpp"],
132 toolchains = [
133 str(Label("//rust:toolchain")),
134 ],
135)
136
137def _rustfmt_test_impl(ctx):
138 # The executable of a test target must be the output of an action in
139 # the rule implementation. This file is simply a symlink to the real
140 # rustfmt test runner.
141 runner = ctx.actions.declare_file("{}{}".format(
142 ctx.label.name,
143 ctx.executable._runner.extension,
144 ))
145
146 ctx.actions.symlink(
147 output = runner,
148 target_file = ctx.executable._runner,
149 is_executable = True,
150 )
151
152 manifests = [target[OutputGroupInfo].rustfmt_manifest for target in ctx.attr.targets]
153 srcs = [depset(_find_rustfmtable_srcs(target)) for target in ctx.attr.targets]
154
155 runfiles = ctx.runfiles(
156 transitive_files = depset(transitive = manifests + srcs),
157 )
158
159 runfiles = runfiles.merge(
160 ctx.attr._runner[DefaultInfo].default_runfiles,
161 )
162
163 return [DefaultInfo(
164 files = depset([runner]),
165 runfiles = runfiles,
166 executable = runner,
167 )]
168
169rustfmt_test = rule(
170 implementation = _rustfmt_test_impl,
171 doc = "A test rule for performing `rustfmt --check` on a set of targets",
172 attrs = {
173 "targets": attr.label_list(
174 doc = "Rust targets to run `rustfmt --check` on.",
175 providers = [rust_common.crate_info],
176 aspects = [rustfmt_aspect],
177 ),
178 "_runner": attr.label(
179 doc = "The rustfmt test runner",
180 cfg = "exec",
181 executable = True,
182 default = Label("//tools/rustfmt:rustfmt_test"),
183 ),
184 },
185 test = True,
186)