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