blob: db5ef43a9d97538a32f167251e787a4c3a878870 [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
Brian Silverman5f6f2762022-08-13 19:30:05 -070023 if aspect_ctx:
24 # Targets with specifc tags will not be formatted
25 ignore_tags = [
26 "no-format",
27 "no-rustfmt",
28 "norustfmt",
29 ]
30
31 for tag in ignore_tags:
32 if tag in aspect_ctx.rule.attr.tags:
33 return []
Brian Silvermancc09f182022-03-09 15:40:20 -080034
35 crate_info = target[rust_common.crate_info]
36
37 # Filter out any generated files
38 srcs = [src for src in crate_info.srcs.to_list() if src.is_source]
39
40 return srcs
41
42def _generate_manifest(edition, srcs, ctx):
43 # Gather the source paths to non-generated files
44 src_paths = [src.path for src in srcs]
45
46 # Write the rustfmt manifest
47 manifest = ctx.actions.declare_file(ctx.label.name + ".rustfmt")
48 ctx.actions.write(
49 output = manifest,
50 content = "\n".join(src_paths + [
51 edition,
52 ]),
53 )
54
55 return manifest
56
57def _perform_check(edition, srcs, ctx):
58 toolchain = find_toolchain(ctx)
59 config = ctx.file._config
60 marker = ctx.actions.declare_file(ctx.label.name + ".rustfmt.ok")
61
62 args = ctx.actions.args()
63 args.add("--touch-file")
64 args.add(marker)
65 args.add("--")
66 args.add(toolchain.rustfmt)
67 args.add("--config-path")
68 args.add(config)
69 args.add("--edition")
70 args.add(edition)
71 args.add("--check")
72 args.add_all(srcs)
73
74 ctx.actions.run(
75 executable = ctx.executable._process_wrapper,
76 inputs = srcs + [config],
77 outputs = [marker],
78 tools = [toolchain.rustfmt],
79 arguments = [args],
80 mnemonic = "Rustfmt",
81 )
82
83 return marker
84
85def _rustfmt_aspect_impl(target, ctx):
86 srcs = _find_rustfmtable_srcs(target, ctx)
87
88 # If there are no formattable sources, do nothing.
89 if not srcs:
90 return []
91
92 # Parse the edition to use for formatting from the target
93 edition = target[rust_common.crate_info].edition
94
95 manifest = _generate_manifest(edition, srcs, ctx)
96 marker = _perform_check(edition, srcs, ctx)
97
98 return [
99 OutputGroupInfo(
100 rustfmt_manifest = depset([manifest]),
101 rustfmt_checks = depset([marker]),
102 ),
103 ]
104
105rustfmt_aspect = aspect(
106 implementation = _rustfmt_aspect_impl,
107 doc = """\
108This aspect is used to gather information about a crate for use in rustfmt and perform rustfmt checks
109
110Output Groups:
111
112- `rustfmt_manifest`: A manifest used by rustfmt binaries to provide crate specific settings.
113- `rustfmt_checks`: Executes `rustfmt --check` on the specified target.
114
115The build setting `@rules_rust//:rustfmt.toml` is used to control the Rustfmt [configuration settings][cs]
116used at runtime.
117
118[cs]: https://rust-lang.github.io/rustfmt/
119
120This aspect is executed on any target which provides the `CrateInfo` provider. However
Brian Silverman5f6f2762022-08-13 19:30:05 -0700121users may tag a target with `no-rustfmt` or `no-format` to have it skipped. Additionally,
122generated source files are also ignored by this aspect.
Brian Silvermancc09f182022-03-09 15:40:20 -0800123""",
124 attrs = {
125 "_config": attr.label(
126 doc = "The `rustfmt.toml` file used for formatting",
127 allow_single_file = True,
128 default = Label("//:rustfmt.toml"),
129 ),
130 "_process_wrapper": attr.label(
131 doc = "A process wrapper for running rustfmt on all platforms",
132 cfg = "exec",
133 executable = True,
134 default = Label("//util/process_wrapper"),
135 ),
136 },
137 incompatible_use_toolchain_transition = True,
138 fragments = ["cpp"],
139 host_fragments = ["cpp"],
140 toolchains = [
Brian Silverman5f6f2762022-08-13 19:30:05 -0700141 str(Label("//rust:toolchain_type")),
Brian Silvermancc09f182022-03-09 15:40:20 -0800142 ],
143)
144
145def _rustfmt_test_impl(ctx):
146 # The executable of a test target must be the output of an action in
147 # the rule implementation. This file is simply a symlink to the real
148 # rustfmt test runner.
Brian Silverman5f6f2762022-08-13 19:30:05 -0700149 is_windows = ctx.executable._runner.extension == ".exe"
Brian Silvermancc09f182022-03-09 15:40:20 -0800150 runner = ctx.actions.declare_file("{}{}".format(
151 ctx.label.name,
Brian Silverman5f6f2762022-08-13 19:30:05 -0700152 ".exe" if is_windows else "",
Brian Silvermancc09f182022-03-09 15:40:20 -0800153 ))
154
155 ctx.actions.symlink(
156 output = runner,
157 target_file = ctx.executable._runner,
158 is_executable = True,
159 )
160
Brian Silverman5f6f2762022-08-13 19:30:05 -0700161 manifests = depset(transitive = [target[OutputGroupInfo].rustfmt_manifest for target in ctx.attr.targets])
Brian Silvermancc09f182022-03-09 15:40:20 -0800162 srcs = [depset(_find_rustfmtable_srcs(target)) for target in ctx.attr.targets]
163
164 runfiles = ctx.runfiles(
Brian Silverman5f6f2762022-08-13 19:30:05 -0700165 transitive_files = depset(transitive = srcs + [manifests]),
Brian Silvermancc09f182022-03-09 15:40:20 -0800166 )
167
168 runfiles = runfiles.merge(
169 ctx.attr._runner[DefaultInfo].default_runfiles,
170 )
171
Brian Silverman5f6f2762022-08-13 19:30:05 -0700172 path_env_sep = ";" if is_windows else ":"
173
174 return [
175 DefaultInfo(
176 files = depset([runner]),
177 runfiles = runfiles,
178 executable = runner,
179 ),
180 testing.TestEnvironment({
181 "RUSTFMT_MANIFESTS": path_env_sep.join([
182 manifest.short_path
183 for manifest in sorted(manifests.to_list())
184 ]),
185 "RUST_BACKTRACE": "1",
186 }),
187 ]
Brian Silvermancc09f182022-03-09 15:40:20 -0800188
189rustfmt_test = rule(
190 implementation = _rustfmt_test_impl,
191 doc = "A test rule for performing `rustfmt --check` on a set of targets",
192 attrs = {
193 "targets": attr.label_list(
194 doc = "Rust targets to run `rustfmt --check` on.",
195 providers = [rust_common.crate_info],
196 aspects = [rustfmt_aspect],
197 ),
198 "_runner": attr.label(
199 doc = "The rustfmt test runner",
200 cfg = "exec",
201 executable = True,
202 default = Label("//tools/rustfmt:rustfmt_test"),
203 ),
204 },
205 test = True,
206)
Brian Silverman5f6f2762022-08-13 19:30:05 -0700207
208def _rustfmt_workspace_name_impl(ctx):
209 output = ctx.actions.declare_file(ctx.label.name)
210
211 ctx.actions.write(
212 output = output,
213 content = "RUSTFMT_WORKSPACE={}".format(
214 ctx.workspace_name,
215 ),
216 )
217
218 return [DefaultInfo(
219 files = depset([output]),
220 )]
221
222rustfmt_workspace_name = rule(
223 implementation = _rustfmt_workspace_name_impl,
224 doc = "A rule for detecting the workspace name for Rustfmt runfiles.",
225)