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