xref: /aosp_15_r20/external/bazelbuild-rules_rust/rust/private/unpretty.bzl (revision d4726bddaa87cc4778e7472feed243fa4b6c267f)
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, _ = collect_deps(
136        deps = crate_info.deps,
137        proc_macro_deps = crate_info.proc_macro_deps,
138        aliases = crate_info.aliases,
139    )
140
141    compile_inputs, out_dir, build_env_files, build_flags_files, linkstamp_outs, ambiguous_libs = collect_inputs(
142        ctx,
143        ctx.rule.file,
144        ctx.rule.files,
145        # Rust expand doesn't need to invoke transitive linking, therefore doesn't need linkstamps.
146        depset([]),
147        toolchain,
148        cc_toolchain,
149        feature_configuration,
150        crate_info,
151        dep_info,
152        build_info,
153    )
154
155    output_groups = {}
156    outputs = []
157
158    for mode in ctx.attr._unpretty_modes[RustUnprettyInfo].modes.to_list():
159        pretty_mode = mode.replace("-", "_")
160        mnemonic = "RustUnpretty{}".format("".join([
161            o.title()
162            for m in pretty_mode.split(",")
163            for o in m.split("_")
164        ]))
165
166        unpretty_out = ctx.actions.declare_file(
167            "{}.unpretty.{}.rs".format(ctx.label.name, pretty_mode.replace(",", ".")),
168            sibling = crate_info.output,
169        )
170
171        output_groups.update({"rust_unpretty_{}".format(pretty_mode.replace(",", "_")): depset([unpretty_out])})
172        outputs.append(unpretty_out)
173
174        rust_flags = []
175        if crate_info.is_test:
176            rust_flags = get_rust_test_flags(ctx.rule.attr)
177
178        args, env = construct_arguments(
179            ctx = ctx,
180            attr = ctx.rule.attr,
181            file = ctx.file,
182            toolchain = toolchain,
183            tool_path = toolchain.rustc.path,
184            cc_toolchain = cc_toolchain,
185            feature_configuration = feature_configuration,
186            crate_info = crate_info,
187            dep_info = dep_info,
188            linkstamp_outs = linkstamp_outs,
189            ambiguous_libs = ambiguous_libs,
190            output_hash = determine_output_hash(crate_info.root, ctx.label),
191            rust_flags = rust_flags,
192            out_dir = out_dir,
193            build_env_files = build_env_files,
194            build_flags_files = build_flags_files,
195            emit = ["dep-info", "metadata"],
196            skip_expanding_rustc_env = True,
197        )
198
199        args.process_wrapper_flags.add("--stdout-file", unpretty_out)
200
201        # Expand all macros and dump the source to stdout.
202        # Tracking issue: https://github.com/rust-lang/rust/issues/43364
203        args.rustc_flags.add("-Zunpretty={}".format(mode))
204
205        ctx.actions.run(
206            executable = ctx.executable._process_wrapper,
207            inputs = compile_inputs,
208            outputs = [unpretty_out],
209            env = env,
210            arguments = args.all,
211            mnemonic = mnemonic,
212            toolchain = "@rules_rust//rust:toolchain_type",
213        )
214
215    output_groups.update({"rust_unpretty": depset(outputs)})
216
217    return [
218        OutputGroupInfo(**output_groups),
219    ]
220
221# Example: Expand all rust targets in the codebase.
222#   bazel build --aspects=@rules_rust//rust:defs.bzl%rust_unpretty_aspect \
223#               --output_groups=expanded \
224#               //...
225rust_unpretty_aspect = aspect(
226    implementation = _rust_unpretty_aspect_impl,
227    fragments = ["cpp"],
228    attrs = {
229        "_unpretty_modes": attr.label(
230            doc = "The values to pass to `--unpretty`",
231            providers = [RustUnprettyInfo],
232            default = Label("//rust/settings:unpretty"),
233        ),
234    } | RUSTC_ATTRS,
235    toolchains = [
236        str(Label("//rust:toolchain_type")),
237        "@bazel_tools//tools/cpp:toolchain_type",
238    ],
239    required_providers = [rust_common.crate_info],
240    doc = """\
241Executes Rust expand on specified targets.
242
243This aspect applies to existing rust_library, rust_test, and rust_binary rules.
244
245As an example, if the following is defined in `examples/hello_lib/BUILD.bazel`:
246
247```python
248load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test")
249
250rust_library(
251    name = "hello_lib",
252    srcs = ["src/lib.rs"],
253)
254
255rust_test(
256    name = "greeting_test",
257    srcs = ["tests/greeting.rs"],
258    deps = [":hello_lib"],
259)
260```
261
262Then the targets can be expanded with the following command:
263
264```output
265$ bazel build --aspects=@rules_rust//rust:defs.bzl%rust_unpretty_aspect \
266              --output_groups=rust_unpretty_expanded //hello_lib:all
267```
268""",
269)
270
271def _rust_unpretty_rule_impl(ctx):
272    mode = ctx.attr.mode
273    output_group = "rust_unpretty_{}".format(mode.replace(",", "_").replace("-", "_"))
274    files = []
275    for target in ctx.attr.deps:
276        files.append(getattr(target[OutputGroupInfo], output_group))
277
278    return [DefaultInfo(files = depset(transitive = files))]
279
280rust_unpretty = rule(
281    implementation = _rust_unpretty_rule_impl,
282    cfg = nightly_unpretty_transition,
283    attrs = {
284        "deps": attr.label_list(
285            doc = "Rust targets to run unpretty on.",
286            providers = [rust_common.crate_info],
287            aspects = [rust_unpretty_aspect],
288        ),
289        "mode": attr.string(
290            doc = "The value to pass to `--unpretty`",
291            values = _UNPRETTY_MODES,
292            default = "expanded",
293        ),
294        "_allowlist_function_transition": attr.label(
295            default = Label("//tools/allowlists/function_transition_allowlist"),
296        ),
297        "_unpretty_modes": attr.label(
298            doc = "The values to pass to `--unpretty`",
299            providers = [RustUnprettyInfo],
300            default = Label("//rust/settings:unpretty"),
301        ),
302    },
303    doc = """\
304Executes rust unpretty on a specific target.
305
306Similar to `rust_unpretty_aspect`, but allows specifying a list of dependencies \
307within the build system.
308
309For example, given the following example targets:
310
311```python
312load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test")
313
314rust_library(
315    name = "hello_lib",
316    srcs = ["src/lib.rs"],
317)
318
319rust_test(
320    name = "greeting_test",
321    srcs = ["tests/greeting.rs"],
322    deps = [":hello_lib"],
323)
324```
325
326Rust expand can be set as a build target with the following:
327
328```python
329load("@rules_rust//rust:defs.bzl", "rust_unpretty")
330
331rust_unpretty(
332    name = "hello_library_expand",
333    testonly = True,
334    deps = [
335        ":hello_lib",
336        ":greeting_test",
337    ],
338    mode = "expand",
339)
340```
341""",
342)
343