• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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_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.
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    # 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):
71    rustfmt_toolchain = ctx.toolchains[Label("//rust/rustfmt:toolchain_type")]
72
73    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", marker)
78    args.add("--")
79    args.add(rustfmt_toolchain.rustfmt)
80    args.add("--config-path", config)
81    args.add("--edition", edition)
82    args.add("--check")
83    args.add_all(srcs)
84
85    ctx.actions.run(
86        executable = ctx.executable._process_wrapper,
87        inputs = srcs + [config],
88        outputs = [marker],
89        tools = [rustfmt_toolchain.all_files],
90        arguments = [args],
91        mnemonic = "Rustfmt",
92    )
93
94    return marker
95
96def _rustfmt_aspect_impl(target, ctx):
97    crate_info = _get_rustfmt_ready_crate_info(target)
98
99    if not crate_info:
100        return []
101
102    srcs = _find_rustfmtable_srcs(crate_info, ctx)
103
104    # If there are no formattable sources, do nothing.
105    if not srcs:
106        return []
107
108    edition = crate_info.edition
109
110    marker = _perform_check(edition, srcs, ctx)
111
112    return [
113        OutputGroupInfo(
114            rustfmt_checks = depset([marker]),
115        ),
116    ]
117
118rustfmt_aspect = aspect(
119    implementation = _rustfmt_aspect_impl,
120    doc = """\
121This aspect is used to gather information about a crate for use in rustfmt and perform rustfmt checks
122
123Output Groups:
124
125- `rustfmt_checks`: Executes `rustfmt --check` on the specified target.
126
127The build setting `@rules_rust//:rustfmt.toml` is used to control the Rustfmt [configuration settings][cs]
128used at runtime.
129
130[cs]: https://rust-lang.github.io/rustfmt/
131
132This aspect is executed on any target which provides the `CrateInfo` provider. However
133users may tag a target with `no-rustfmt` or `no-format` to have it skipped. Additionally,
134generated source files are also ignored by this aspect.
135""",
136    attrs = {
137        "_config": attr.label(
138            doc = "The `rustfmt.toml` file used for formatting",
139            allow_single_file = True,
140            default = Label("//:rustfmt.toml"),
141        ),
142        "_process_wrapper": attr.label(
143            doc = "A process wrapper for running rustfmt on all platforms",
144            cfg = "exec",
145            executable = True,
146            default = Label("//util/process_wrapper"),
147        ),
148    },
149    required_providers = [
150        [rust_common.crate_info],
151        [rust_common.test_crate_info],
152    ],
153    fragments = ["cpp"],
154    toolchains = [
155        str(Label("//rust/rustfmt:toolchain_type")),
156    ],
157)
158
159def _rustfmt_test_manifest_aspect_impl(target, ctx):
160    crate_info = _get_rustfmt_ready_crate_info(target)
161
162    if not crate_info:
163        return []
164
165    # Parse the edition to use for formatting from the target
166    edition = crate_info.edition
167
168    srcs = _find_rustfmtable_srcs(crate_info, ctx)
169    manifest = _generate_manifest(edition, srcs, ctx)
170
171    return [
172        OutputGroupInfo(
173            rustfmt_manifest = depset([manifest]),
174        ),
175    ]
176
177# This aspect contains functionality split out of `rustfmt_aspect` which broke when
178# `required_providers` was added to it. Aspects which have `required_providers` seems
179# to not function with attributes that also require providers.
180_rustfmt_test_manifest_aspect = aspect(
181    implementation = _rustfmt_test_manifest_aspect_impl,
182    doc = """\
183This aspect is used to gather information about a crate for use in `rustfmt_test`
184
185Output Groups:
186
187- `rustfmt_manifest`: A manifest used by rustfmt binaries to provide crate specific settings.
188""",
189    fragments = ["cpp"],
190    toolchains = [
191        str(Label("//rust/rustfmt:toolchain_type")),
192    ],
193)
194
195def _rustfmt_test_impl(ctx):
196    # The executable of a test target must be the output of an action in
197    # the rule implementation. This file is simply a symlink to the real
198    # rustfmt test runner.
199    is_windows = ctx.executable._runner.extension == ".exe"
200    runner = ctx.actions.declare_file("{}{}".format(
201        ctx.label.name,
202        ".exe" if is_windows else "",
203    ))
204
205    ctx.actions.symlink(
206        output = runner,
207        target_file = ctx.executable._runner,
208        is_executable = True,
209    )
210
211    crate_infos = [_get_rustfmt_ready_crate_info(target) for target in ctx.attr.targets]
212    srcs = [depset(_find_rustfmtable_srcs(crate_info)) for crate_info in crate_infos if crate_info]
213
214    # Some targets may be included in tests but tagged as "no-format". In this
215    # case, there will be no manifest.
216    manifests = [getattr(target[OutputGroupInfo], "rustfmt_manifest", None) for target in ctx.attr.targets]
217    manifests = depset(transitive = [manifest for manifest in manifests if manifest])
218
219    runfiles = ctx.runfiles(
220        transitive_files = depset(transitive = srcs + [manifests]),
221    )
222
223    runfiles = runfiles.merge(
224        ctx.attr._runner[DefaultInfo].default_runfiles,
225    )
226
227    path_env_sep = ";" if is_windows else ":"
228
229    return [
230        DefaultInfo(
231            files = depset([runner]),
232            runfiles = runfiles,
233            executable = runner,
234        ),
235        testing.TestEnvironment({
236            "RUSTFMT_MANIFESTS": path_env_sep.join([
237                manifest.short_path
238                for manifest in sorted(manifests.to_list())
239            ]),
240            "RUST_BACKTRACE": "1",
241        }),
242    ]
243
244rustfmt_test = rule(
245    implementation = _rustfmt_test_impl,
246    doc = "A test rule for performing `rustfmt --check` on a set of targets",
247    attrs = {
248        "targets": attr.label_list(
249            doc = "Rust targets to run `rustfmt --check` on.",
250            providers = [
251                [rust_common.crate_info],
252                [rust_common.test_crate_info],
253            ],
254            aspects = [_rustfmt_test_manifest_aspect],
255        ),
256        "_runner": attr.label(
257            doc = "The rustfmt test runner",
258            cfg = "exec",
259            executable = True,
260            default = Label("//tools/rustfmt:rustfmt_test"),
261        ),
262    },
263    test = True,
264)
265
266def _rustfmt_toolchain_impl(ctx):
267    make_variables = {
268        "RUSTFMT": ctx.file.rustfmt.path,
269    }
270
271    if ctx.attr.rustc:
272        make_variables.update({
273            "RUSTC": ctx.file.rustc.path,
274        })
275
276    make_variable_info = platform_common.TemplateVariableInfo(make_variables)
277
278    all_files = [ctx.file.rustfmt] + ctx.files.rustc_lib
279    if ctx.file.rustc:
280        all_files.append(ctx.file.rustc)
281
282    toolchain = platform_common.ToolchainInfo(
283        rustfmt = ctx.file.rustfmt,
284        rustc = ctx.file.rustc,
285        rustc_lib = depset(ctx.files.rustc_lib),
286        all_files = depset(all_files),
287        make_variables = make_variable_info,
288    )
289
290    return [
291        toolchain,
292        make_variable_info,
293    ]
294
295rustfmt_toolchain = rule(
296    doc = "A toolchain for [rustfmt](https://rust-lang.github.io/rustfmt/)",
297    implementation = _rustfmt_toolchain_impl,
298    attrs = {
299        "rustc": attr.label(
300            doc = "The location of the `rustc` binary. Can be a direct source or a filegroup containing one item.",
301            allow_single_file = True,
302            cfg = "exec",
303        ),
304        "rustc_lib": attr.label(
305            doc = "The libraries used by rustc during compilation.",
306            cfg = "exec",
307        ),
308        "rustfmt": attr.label(
309            doc = "The location of the `rustfmt` binary. Can be a direct source or a filegroup containing one item.",
310            allow_single_file = True,
311            cfg = "exec",
312            mandatory = True,
313        ),
314    },
315    toolchains = [
316        str(Label("@rules_rust//rust:toolchain_type")),
317    ],
318)
319
320def _current_rustfmt_toolchain_impl(ctx):
321    toolchain = ctx.toolchains[str(Label("@rules_rust//rust/rustfmt:toolchain_type"))]
322
323    return [
324        toolchain,
325        toolchain.make_variables,
326        DefaultInfo(
327            files = depset([
328                toolchain.rustfmt,
329            ]),
330            runfiles = ctx.runfiles(transitive_files = toolchain.all_files),
331        ),
332    ]
333
334current_rustfmt_toolchain = rule(
335    doc = "A rule for exposing the current registered `rustfmt_toolchain`.",
336    implementation = _current_rustfmt_toolchain_impl,
337    toolchains = [
338        str(Label("@rules_rust//rust/rustfmt:toolchain_type")),
339    ],
340)
341