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