• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"""A module defining Rust 'unpretty' rules"""
2
3load("//rust/private:common.bzl", "rust_common")
4load(
5    "//rust/private:rust.bzl",
6    "RUSTC_ATTRS",
7    "get_rust_test_flags",
8)
9load(
10    "//rust/private:rustc.bzl",
11    "collect_deps",
12    "collect_inputs",
13    "construct_arguments",
14)
15load(
16    "//rust/private:utils.bzl",
17    "determine_output_hash",
18    "find_cc_toolchain",
19    "find_toolchain",
20)
21
22# This list is determined by running the following command:
23#
24#   rustc +nightly -Zunpretty=
25#
26_UNPRETTY_MODES = [
27    "ast-tree,expanded",
28    "ast-tree",
29    "expanded,hygiene",
30    "expanded,identified",
31    "expanded",
32    "hir-tree",
33    "hir,identified",
34    "hir,typed",
35    "hir",
36    "identified",
37    "mir-cfg",
38    "mir",
39    "normal",
40]
41
42RustUnprettyInfo = provider(
43    doc = "A provider describing the Rust unpretty mode.",
44    fields = {
45        "modes": "Depset[string]: Can be any of {}".format(["'{}'".format(m) for m in _UNPRETTY_MODES]),
46    },
47)
48
49def _rust_unpretty_flag_impl(ctx):
50    value = ctx.build_setting_value
51    invalid = []
52    for mode in value:
53        if mode not in _UNPRETTY_MODES:
54            invalid.append(mode)
55    if invalid:
56        fail("{} build setting allowed to take values [{}] but was set to unallowed values: {}".format(
57            ctx.label,
58            ", ".join(["'{}'".format(m) for m in _UNPRETTY_MODES]),
59            invalid,
60        ))
61
62    return RustUnprettyInfo(modes = depset(value))
63
64rust_unpretty_flag = rule(
65    doc = "A build setting which represents the Rust unpretty mode. The allowed values are {}".format(_UNPRETTY_MODES),
66    implementation = _rust_unpretty_flag_impl,
67    build_setting = config.string_list(
68        flag = True,
69        repeatable = True,
70    ),
71)
72
73def _nightly_unpretty_transition_impl(settings, attr):
74    mode = settings[str(Label("//rust/settings:unpretty"))]
75
76    # Use the presence of _unpretty_modes as a proxy for whether this is a rust_unpretty target.
77    if hasattr(attr, "_unpretty_modes") and hasattr(attr, "mode"):
78        mode = mode + [attr.mode]
79
80    return {
81        str(Label("//rust/settings:unpretty")): depset(mode).to_list(),
82        str(Label("//rust/toolchain/channel")): "nightly",
83    }
84
85nightly_unpretty_transition = transition(
86    implementation = _nightly_unpretty_transition_impl,
87    inputs = [str(Label("//rust/settings:unpretty"))],
88    outputs = [
89        str(Label("//rust/settings:unpretty")),
90        str(Label("//rust/toolchain/channel")),
91    ],
92)
93
94def _get_unpretty_ready_crate_info(target, aspect_ctx):
95    """Check that a target is suitable for expansion and extract the `CrateInfo` provider from it.
96
97    Args:
98        target (Target): The target the aspect is running on.
99        aspect_ctx (ctx, optional): The aspect's context object.
100
101    Returns:
102        CrateInfo, optional: A `CrateInfo` provider if rust unpretty should be run or `None`.
103    """
104
105    # Ignore external targets
106    if target.label.workspace_root.startswith("external"):
107        return None
108
109    # Targets with specific tags will not be formatted
110    if aspect_ctx:
111        ignore_tags = [
112            "nounpretty",
113            "no-unpretty",
114            "no_unpretty",
115        ]
116
117        for tag in ignore_tags:
118            if tag in aspect_ctx.rule.attr.tags:
119                return None
120
121    # Obviously ignore any targets that don't contain `CrateInfo`
122    if rust_common.crate_info not in target:
123        return None
124
125    return target[rust_common.crate_info]
126
127def _rust_unpretty_aspect_impl(target, ctx):
128    crate_info = _get_unpretty_ready_crate_info(target, ctx)
129    if not crate_info:
130        return []
131
132    toolchain = find_toolchain(ctx)
133    cc_toolchain, feature_configuration = find_cc_toolchain(ctx)
134
135    dep_info, build_info, linkstamps = collect_deps(
136        deps = crate_info.deps,
137        proc_macro_deps = crate_info.proc_macro_deps,
138        aliases = crate_info.aliases,
139        # Rust expand doesn't need to invoke transitive linking, therefore doesn't need linkstamps.
140        are_linkstamps_supported = False,
141    )
142
143    compile_inputs, out_dir, build_env_files, build_flags_files, linkstamp_outs, ambiguous_libs = collect_inputs(
144        ctx,
145        ctx.rule.file,
146        ctx.rule.files,
147        linkstamps,
148        toolchain,
149        cc_toolchain,
150        feature_configuration,
151        crate_info,
152        dep_info,
153        build_info,
154    )
155
156    output_groups = {}
157    outputs = []
158
159    for mode in ctx.attr._unpretty_modes[RustUnprettyInfo].modes.to_list():
160        pretty_mode = mode.replace("-", "_")
161        mnemonic = "RustUnpretty{}".format("".join([
162            o.title()
163            for m in pretty_mode.split(",")
164            for o in m.split("_")
165        ]))
166
167        unpretty_out = ctx.actions.declare_file(
168            "{}.unpretty.{}.rs".format(ctx.label.name, pretty_mode.replace(",", ".")),
169            sibling = crate_info.output,
170        )
171
172        output_groups.update({"rust_unpretty_{}".format(pretty_mode.replace(",", "_")): depset([unpretty_out])})
173        outputs.append(unpretty_out)
174
175        rust_flags = []
176        if crate_info.is_test:
177            rust_flags = get_rust_test_flags(ctx.rule.attr)
178
179        args, env = construct_arguments(
180            ctx = ctx,
181            attr = ctx.rule.attr,
182            file = ctx.file,
183            toolchain = toolchain,
184            tool_path = toolchain.rustc.path,
185            cc_toolchain = cc_toolchain,
186            feature_configuration = feature_configuration,
187            crate_info = crate_info,
188            dep_info = dep_info,
189            linkstamp_outs = linkstamp_outs,
190            ambiguous_libs = ambiguous_libs,
191            output_hash = determine_output_hash(crate_info.root, ctx.label),
192            rust_flags = rust_flags,
193            out_dir = out_dir,
194            build_env_files = build_env_files,
195            build_flags_files = build_flags_files,
196            emit = ["dep-info", "metadata"],
197            skip_expanding_rustc_env = True,
198        )
199
200        args.process_wrapper_flags.add("--stdout-file", unpretty_out)
201
202        # Expand all macros and dump the source to stdout.
203        # Tracking issue: https://github.com/rust-lang/rust/issues/43364
204        args.rustc_flags.add("-Zunpretty={}".format(mode))
205
206        ctx.actions.run(
207            executable = ctx.executable._process_wrapper,
208            inputs = compile_inputs,
209            outputs = [unpretty_out],
210            env = env,
211            arguments = args.all,
212            mnemonic = mnemonic,
213            toolchain = "@rules_rust//rust:toolchain_type",
214        )
215
216    output_groups.update({"rust_unpretty": depset(outputs)})
217
218    return [
219        OutputGroupInfo(**output_groups),
220    ]
221
222# Example: Expand all rust targets in the codebase.
223#   bazel build --aspects=@rules_rust//rust:defs.bzl%rust_unpretty_aspect \
224#               --output_groups=expanded \
225#               //...
226rust_unpretty_aspect = aspect(
227    implementation = _rust_unpretty_aspect_impl,
228    fragments = ["cpp"],
229    attrs = {
230        "_unpretty_modes": attr.label(
231            doc = "The values to pass to `--unpretty`",
232            providers = [RustUnprettyInfo],
233            default = Label("//rust/settings:unpretty"),
234        ),
235    } | RUSTC_ATTRS,
236    toolchains = [
237        str(Label("//rust:toolchain_type")),
238        "@bazel_tools//tools/cpp:toolchain_type",
239    ],
240    required_providers = [rust_common.crate_info],
241    doc = """\
242Executes Rust expand on specified targets.
243
244This aspect applies to existing rust_library, rust_test, and rust_binary rules.
245
246As an example, if the following is defined in `examples/hello_lib/BUILD.bazel`:
247
248```python
249load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test")
250
251rust_library(
252    name = "hello_lib",
253    srcs = ["src/lib.rs"],
254)
255
256rust_test(
257    name = "greeting_test",
258    srcs = ["tests/greeting.rs"],
259    deps = [":hello_lib"],
260)
261```
262
263Then the targets can be expanded with the following command:
264
265```output
266$ bazel build --aspects=@rules_rust//rust:defs.bzl%rust_unpretty_aspect \
267              --output_groups=rust_unpretty_expanded //hello_lib:all
268```
269""",
270)
271
272def _rust_unpretty_rule_impl(ctx):
273    mode = ctx.attr.mode
274    output_group = "rust_unpretty_{}".format(mode.replace(",", "_").replace("-", "_"))
275    files = []
276    for target in ctx.attr.deps:
277        files.append(getattr(target[OutputGroupInfo], output_group))
278
279    return [DefaultInfo(files = depset(transitive = files))]
280
281rust_unpretty = rule(
282    implementation = _rust_unpretty_rule_impl,
283    cfg = nightly_unpretty_transition,
284    attrs = {
285        "deps": attr.label_list(
286            doc = "Rust targets to run unpretty on.",
287            providers = [rust_common.crate_info],
288            aspects = [rust_unpretty_aspect],
289        ),
290        "mode": attr.string(
291            doc = "The value to pass to `--unpretty`",
292            values = _UNPRETTY_MODES,
293            default = "expanded",
294        ),
295        "_allowlist_function_transition": attr.label(
296            default = Label("//tools/allowlists/function_transition_allowlist"),
297        ),
298        "_unpretty_modes": attr.label(
299            doc = "The values to pass to `--unpretty`",
300            providers = [RustUnprettyInfo],
301            default = Label("//rust/settings:unpretty"),
302        ),
303    },
304    doc = """\
305Executes rust unpretty on a specific target.
306
307Similar to `rust_unpretty_aspect`, but allows specifying a list of dependencies \
308within the build system.
309
310For example, given the following example targets:
311
312```python
313load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test")
314
315rust_library(
316    name = "hello_lib",
317    srcs = ["src/lib.rs"],
318)
319
320rust_test(
321    name = "greeting_test",
322    srcs = ["tests/greeting.rs"],
323    deps = [":hello_lib"],
324)
325```
326
327Rust expand can be set as a build target with the following:
328
329```python
330load("@rules_rust//rust:defs.bzl", "rust_unpretty")
331
332rust_unpretty(
333    name = "hello_library_expand",
334    testonly = True,
335    deps = [
336        ":hello_lib",
337        ":greeting_test",
338    ],
339    mode = "expand",
340)
341```
342""",
343)
344