xref: /aosp_15_r20/external/bazelbuild-rules_rust/rust/private/rustfmt.bzl (revision d4726bddaa87cc4778e7472feed243fa4b6c267f)
1"""A module defining rustfmt rules"""
2
3load(":common.bzl", "rust_common")
4
5def _get_rustfmt_ready_crate_info(target):
6    """Check that a target is suitable for rustfmt and extract the `CrateInfo` provider from it.
7
8    Args:
9        target (Target): The target the aspect is running on.
10
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_name:
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.
32        aspect_ctx (ctx, optional): The aspect's context object.
33
34    Returns:
35        list: A list of formattable sources (`File`).
36    """
37
38    # Targets with specific tags will not be formatted
39    if aspect_ctx:
40        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 []
49
50    # 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    workspace = ctx.label.workspace_name or ctx.workspace_name
57
58    # Gather the source paths to non-generated files
59    content = ctx.actions.args()
60    content.set_param_file_format("multiline")
61    content.add_all(srcs, format_each = workspace + "/%s")
62    content.add(edition)
63
64    # Write the rustfmt manifest
65    manifest = ctx.actions.declare_file(ctx.label.name + ".rustfmt")
66    ctx.actions.write(
67        output = manifest,
68        content = content,
69    )
70
71    return manifest
72
73def _perform_check(edition, srcs, ctx):
74    rustfmt_toolchain = ctx.toolchains[Label("//rust/rustfmt:toolchain_type")]
75
76    config = ctx.file._config
77    marker = ctx.actions.declare_file(ctx.label.name + ".rustfmt.ok")
78
79    args = ctx.actions.args()
80    args.add("--touch-file", marker)
81    args.add("--")
82    args.add(rustfmt_toolchain.rustfmt)
83    args.add("--config-path", config)
84    args.add("--edition", 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],
92        tools = [rustfmt_toolchain.all_files],
93        arguments = [args],
94        mnemonic = "Rustfmt",
95    )
96
97    return marker
98
99def _rustfmt_aspect_impl(target, ctx):
100    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)
106
107    # If there are no formattable sources, do nothing.
108    if not srcs:
109        return []
110
111    edition = crate_info.edition
112
113    marker = _perform_check(edition, srcs, ctx)
114
115    return [
116        OutputGroupInfo(
117            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
128- `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
136users may tag a target with `no-rustfmt` or `no-format` to have it skipped. Additionally,
137generated source files are also ignored by this aspect.
138""",
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    required_providers = [
153        [rust_common.crate_info],
154        [rust_common.test_crate_info],
155    ],
156    fragments = ["cpp"],
157    toolchains = [
158        str(Label("//rust/rustfmt:toolchain_type")),
159    ],
160)
161
162def _rustfmt_test_manifest_aspect_impl(target, ctx):
163    crate_info = _get_rustfmt_ready_crate_info(target)
164
165    if not crate_info:
166        return []
167
168    # Parse the edition to use for formatting from the target
169    edition = crate_info.edition
170
171    srcs = _find_rustfmtable_srcs(crate_info, ctx)
172    manifest = _generate_manifest(edition, srcs, ctx)
173
174    return [
175        OutputGroupInfo(
176            rustfmt_manifest = depset([manifest]),
177        ),
178    ]
179
180# This aspect contains functionality split out of `rustfmt_aspect` which broke when
181# `required_providers` was added to it. Aspects which have `required_providers` seems
182# to not function with attributes that also require providers.
183_rustfmt_test_manifest_aspect = aspect(
184    implementation = _rustfmt_test_manifest_aspect_impl,
185    doc = """\
186This aspect is used to gather information about a crate for use in `rustfmt_test`
187
188Output Groups:
189
190- `rustfmt_manifest`: A manifest used by rustfmt binaries to provide crate specific settings.
191""",
192    fragments = ["cpp"],
193    toolchains = [
194        str(Label("//rust/rustfmt:toolchain_type")),
195    ],
196)
197
198def _rustfmt_test_impl(ctx):
199    # The executable of a test target must be the output of an action in
200    # the rule implementation. This file is simply a symlink to the real
201    # rustfmt test runner.
202    is_windows = ctx.executable._runner.extension == ".exe"
203    runner = ctx.actions.declare_file("{}{}".format(
204        ctx.label.name,
205        ".exe" if is_windows else "",
206    ))
207
208    ctx.actions.symlink(
209        output = runner,
210        target_file = ctx.executable._runner,
211        is_executable = True,
212    )
213
214    crate_infos = [_get_rustfmt_ready_crate_info(target) for target in ctx.attr.targets]
215    srcs = [depset(_find_rustfmtable_srcs(crate_info)) for crate_info in crate_infos if crate_info]
216
217    # Some targets may be included in tests but tagged as "no-format". In this
218    # case, there will be no manifest.
219    manifests = [getattr(target[OutputGroupInfo], "rustfmt_manifest", None) for target in ctx.attr.targets]
220    manifests = depset(transitive = [manifest for manifest in manifests if manifest])
221
222    runfiles = ctx.runfiles(
223        transitive_files = depset(transitive = srcs + [manifests]),
224    )
225
226    runfiles = runfiles.merge(
227        ctx.attr._runner[DefaultInfo].default_runfiles,
228    )
229
230    workspace = ctx.label.workspace_name or ctx.workspace_name
231
232    return [
233        DefaultInfo(
234            files = depset([runner]),
235            runfiles = runfiles,
236            executable = runner,
237        ),
238        testing.TestEnvironment({
239            "RUSTFMT_MANIFESTS": ctx.configuration.host_path_separator.join([
240                workspace + "/" + manifest.short_path
241                for manifest in sorted(manifests.to_list())
242            ]),
243            "RUST_BACKTRACE": "1",
244        }),
245    ]
246
247rustfmt_test = rule(
248    implementation = _rustfmt_test_impl,
249    doc = "A test rule for performing `rustfmt --check` on a set of targets",
250    attrs = {
251        "targets": attr.label_list(
252            doc = "Rust targets to run `rustfmt --check` on.",
253            providers = [
254                [rust_common.crate_info],
255                [rust_common.test_crate_info],
256            ],
257            aspects = [_rustfmt_test_manifest_aspect],
258        ),
259        "_runner": attr.label(
260            doc = "The rustfmt test runner",
261            cfg = "exec",
262            executable = True,
263            default = Label("//tools/rustfmt:rustfmt_test"),
264        ),
265    },
266    test = True,
267)
268
269def _rustfmt_toolchain_impl(ctx):
270    make_variables = {
271        "RUSTFMT": ctx.file.rustfmt.path,
272    }
273
274    if ctx.attr.rustc:
275        make_variables.update({
276            "RUSTC": ctx.file.rustc.path,
277        })
278
279    make_variable_info = platform_common.TemplateVariableInfo(make_variables)
280
281    all_files = [ctx.file.rustfmt] + ctx.files.rustc_lib
282    if ctx.file.rustc:
283        all_files.append(ctx.file.rustc)
284
285    toolchain = platform_common.ToolchainInfo(
286        rustfmt = ctx.file.rustfmt,
287        rustc = ctx.file.rustc,
288        rustc_lib = depset(ctx.files.rustc_lib),
289        all_files = depset(all_files),
290        make_variables = make_variable_info,
291    )
292
293    return [
294        toolchain,
295        make_variable_info,
296    ]
297
298rustfmt_toolchain = rule(
299    doc = "A toolchain for [rustfmt](https://rust-lang.github.io/rustfmt/)",
300    implementation = _rustfmt_toolchain_impl,
301    attrs = {
302        "rustc": attr.label(
303            doc = "The location of the `rustc` binary. Can be a direct source or a filegroup containing one item.",
304            allow_single_file = True,
305            cfg = "exec",
306        ),
307        "rustc_lib": attr.label(
308            doc = "The libraries used by rustc during compilation.",
309            cfg = "exec",
310        ),
311        "rustfmt": attr.label(
312            doc = "The location of the `rustfmt` binary. Can be a direct source or a filegroup containing one item.",
313            allow_single_file = True,
314            cfg = "exec",
315            mandatory = True,
316        ),
317    },
318    toolchains = [
319        str(Label("@rules_rust//rust:toolchain_type")),
320    ],
321)
322
323def _current_rustfmt_toolchain_impl(ctx):
324    toolchain = ctx.toolchains[str(Label("@rules_rust//rust/rustfmt:toolchain_type"))]
325
326    return [
327        toolchain,
328        toolchain.make_variables,
329        DefaultInfo(
330            files = depset([
331                toolchain.rustfmt,
332            ]),
333            runfiles = ctx.runfiles(transitive_files = toolchain.all_files),
334        ),
335    ]
336
337current_rustfmt_toolchain = rule(
338    doc = "A rule for exposing the current registered `rustfmt_toolchain`.",
339    implementation = _current_rustfmt_toolchain_impl,
340    toolchains = [
341        str(Label("@rules_rust//rust/rustfmt:toolchain_type")),
342    ],
343)
344