Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 1 | """A module defining rustfmt rules""" |
| 2 | |
| 3 | load(":common.bzl", "rust_common") |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 4 | |
Adam Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 5 | def _get_rustfmt_ready_crate_info(target): |
| 6 | """Check that a target is suitable for rustfmt and extract the `CrateInfo` provider from it. |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 7 | |
| 8 | Args: |
| 9 | target (Target): The target the aspect is running on. |
Adam Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 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 | |
| 27 | def _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. |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 32 | aspect_ctx (ctx, optional): The aspect's context object. |
| 33 | |
| 34 | Returns: |
| 35 | list: A list of formattable sources (`File`). |
| 36 | """ |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 37 | |
Adam Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 38 | # Targets with specific tags will not be formatted |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 39 | if aspect_ctx: |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 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 [] |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 49 | |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 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 | |
| 55 | def _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 | |
| 70 | def _perform_check(edition, srcs, ctx): |
Adam Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 71 | rustfmt_toolchain = ctx.toolchains[Label("//rust/rustfmt:toolchain_type")] |
| 72 | |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 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") |
| 78 | args.add(marker) |
| 79 | args.add("--") |
Adam Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 80 | args.add(rustfmt_toolchain.rustfmt) |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 81 | args.add("--config-path") |
| 82 | args.add(config) |
| 83 | args.add("--edition") |
| 84 | args.add(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], |
Adam Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 92 | tools = [rustfmt_toolchain.all_files], |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 93 | arguments = [args], |
| 94 | mnemonic = "Rustfmt", |
| 95 | ) |
| 96 | |
| 97 | return marker |
| 98 | |
| 99 | def _rustfmt_aspect_impl(target, ctx): |
Adam Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 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) |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 106 | |
| 107 | # If there are no formattable sources, do nothing. |
| 108 | if not srcs: |
| 109 | return [] |
| 110 | |
Adam Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 111 | edition = crate_info.edition |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 112 | |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 113 | marker = _perform_check(edition, srcs, ctx) |
| 114 | |
| 115 | return [ |
| 116 | OutputGroupInfo( |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 117 | rustfmt_checks = depset([marker]), |
| 118 | ), |
| 119 | ] |
| 120 | |
| 121 | rustfmt_aspect = aspect( |
| 122 | implementation = _rustfmt_aspect_impl, |
| 123 | doc = """\ |
| 124 | This aspect is used to gather information about a crate for use in rustfmt and perform rustfmt checks |
| 125 | |
| 126 | Output Groups: |
| 127 | |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 128 | - `rustfmt_checks`: Executes `rustfmt --check` on the specified target. |
| 129 | |
| 130 | The build setting `@rules_rust//:rustfmt.toml` is used to control the Rustfmt [configuration settings][cs] |
| 131 | used at runtime. |
| 132 | |
| 133 | [cs]: https://rust-lang.github.io/rustfmt/ |
| 134 | |
| 135 | This aspect is executed on any target which provides the `CrateInfo` provider. However |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 136 | users may tag a target with `no-rustfmt` or `no-format` to have it skipped. Additionally, |
| 137 | generated source files are also ignored by this aspect. |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 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 | incompatible_use_toolchain_transition = True, |
Adam Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 153 | required_providers = [ |
| 154 | [rust_common.crate_info], |
| 155 | [rust_common.test_crate_info], |
| 156 | ], |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 157 | fragments = ["cpp"], |
| 158 | host_fragments = ["cpp"], |
| 159 | toolchains = [ |
Adam Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 160 | str(Label("//rust/rustfmt:toolchain_type")), |
| 161 | ], |
| 162 | ) |
| 163 | |
| 164 | def _rustfmt_test_manifest_aspect_impl(target, ctx): |
| 165 | crate_info = _get_rustfmt_ready_crate_info(target) |
| 166 | |
| 167 | if not crate_info: |
| 168 | return [] |
| 169 | |
| 170 | # Parse the edition to use for formatting from the target |
| 171 | edition = crate_info.edition |
| 172 | |
| 173 | srcs = _find_rustfmtable_srcs(crate_info, ctx) |
| 174 | manifest = _generate_manifest(edition, srcs, ctx) |
| 175 | |
| 176 | return [ |
| 177 | OutputGroupInfo( |
| 178 | rustfmt_manifest = depset([manifest]), |
| 179 | ), |
| 180 | ] |
| 181 | |
| 182 | # This aspect contains functionality split out of `rustfmt_aspect` which broke when |
| 183 | # `required_providers` was added to it. Aspects which have `required_providers` seems |
| 184 | # to not function with attributes that also require providers. |
| 185 | _rustfmt_test_manifest_aspect = aspect( |
| 186 | implementation = _rustfmt_test_manifest_aspect_impl, |
| 187 | doc = """\ |
| 188 | This aspect is used to gather information about a crate for use in `rustfmt_test` |
| 189 | |
| 190 | Output Groups: |
| 191 | |
| 192 | - `rustfmt_manifest`: A manifest used by rustfmt binaries to provide crate specific settings. |
| 193 | """, |
| 194 | incompatible_use_toolchain_transition = True, |
| 195 | fragments = ["cpp"], |
| 196 | host_fragments = ["cpp"], |
| 197 | toolchains = [ |
| 198 | str(Label("//rust/rustfmt:toolchain_type")), |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 199 | ], |
| 200 | ) |
| 201 | |
| 202 | def _rustfmt_test_impl(ctx): |
| 203 | # The executable of a test target must be the output of an action in |
| 204 | # the rule implementation. This file is simply a symlink to the real |
| 205 | # rustfmt test runner. |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 206 | is_windows = ctx.executable._runner.extension == ".exe" |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 207 | runner = ctx.actions.declare_file("{}{}".format( |
| 208 | ctx.label.name, |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 209 | ".exe" if is_windows else "", |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 210 | )) |
| 211 | |
| 212 | ctx.actions.symlink( |
| 213 | output = runner, |
| 214 | target_file = ctx.executable._runner, |
| 215 | is_executable = True, |
| 216 | ) |
| 217 | |
Adam Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 218 | crate_infos = [_get_rustfmt_ready_crate_info(target) for target in ctx.attr.targets] |
| 219 | srcs = [depset(_find_rustfmtable_srcs(crate_info)) for crate_info in crate_infos if crate_info] |
| 220 | |
| 221 | # Some targets may be included in tests but tagged as "no-format". In this |
| 222 | # case, there will be no manifest. |
| 223 | manifests = [getattr(target[OutputGroupInfo], "rustfmt_manifest", None) for target in ctx.attr.targets] |
| 224 | manifests = depset(transitive = [manifest for manifest in manifests if manifest]) |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 225 | |
| 226 | runfiles = ctx.runfiles( |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 227 | transitive_files = depset(transitive = srcs + [manifests]), |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 228 | ) |
| 229 | |
| 230 | runfiles = runfiles.merge( |
| 231 | ctx.attr._runner[DefaultInfo].default_runfiles, |
| 232 | ) |
| 233 | |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 234 | path_env_sep = ";" if is_windows else ":" |
| 235 | |
| 236 | return [ |
| 237 | DefaultInfo( |
| 238 | files = depset([runner]), |
| 239 | runfiles = runfiles, |
| 240 | executable = runner, |
| 241 | ), |
| 242 | testing.TestEnvironment({ |
| 243 | "RUSTFMT_MANIFESTS": path_env_sep.join([ |
| 244 | manifest.short_path |
| 245 | for manifest in sorted(manifests.to_list()) |
| 246 | ]), |
| 247 | "RUST_BACKTRACE": "1", |
| 248 | }), |
| 249 | ] |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 250 | |
| 251 | rustfmt_test = rule( |
| 252 | implementation = _rustfmt_test_impl, |
| 253 | doc = "A test rule for performing `rustfmt --check` on a set of targets", |
| 254 | attrs = { |
| 255 | "targets": attr.label_list( |
| 256 | doc = "Rust targets to run `rustfmt --check` on.", |
Adam Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 257 | providers = [ |
| 258 | [rust_common.crate_info], |
| 259 | [rust_common.test_crate_info], |
| 260 | ], |
| 261 | aspects = [_rustfmt_test_manifest_aspect], |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 262 | ), |
| 263 | "_runner": attr.label( |
| 264 | doc = "The rustfmt test runner", |
| 265 | cfg = "exec", |
| 266 | executable = True, |
| 267 | default = Label("//tools/rustfmt:rustfmt_test"), |
| 268 | ), |
| 269 | }, |
| 270 | test = True, |
| 271 | ) |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 272 | |
Adam Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 273 | def _rustfmt_toolchain_impl(ctx): |
| 274 | make_variables = { |
| 275 | "RUSTFMT": ctx.file.rustfmt.path, |
| 276 | } |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 277 | |
Adam Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 278 | if ctx.attr.rustc: |
| 279 | make_variables.update({ |
| 280 | "RUSTC": ctx.file.rustc.path, |
| 281 | }) |
| 282 | |
| 283 | make_variable_info = platform_common.TemplateVariableInfo(make_variables) |
| 284 | |
| 285 | all_files = [ctx.file.rustfmt] + ctx.files.rustc_lib |
| 286 | if ctx.file.rustc: |
| 287 | all_files.append(ctx.file.rustc) |
| 288 | |
| 289 | toolchain = platform_common.ToolchainInfo( |
| 290 | rustfmt = ctx.file.rustfmt, |
| 291 | rustc = ctx.file.rustc, |
| 292 | rustc_lib = depset(ctx.files.rustc_lib), |
| 293 | all_files = depset(all_files), |
| 294 | make_variables = make_variable_info, |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 295 | ) |
| 296 | |
Adam Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 297 | return [ |
| 298 | toolchain, |
| 299 | make_variable_info, |
| 300 | ] |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 301 | |
Adam Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 302 | rustfmt_toolchain = rule( |
| 303 | doc = "A toolchain for [rustfmt](https://rust-lang.github.io/rustfmt/)", |
| 304 | implementation = _rustfmt_toolchain_impl, |
| 305 | incompatible_use_toolchain_transition = True, |
| 306 | attrs = { |
| 307 | "rustc": attr.label( |
| 308 | doc = "The location of the `rustc` binary. Can be a direct source or a filegroup containing one item.", |
| 309 | allow_single_file = True, |
| 310 | cfg = "exec", |
| 311 | ), |
| 312 | "rustc_lib": attr.label( |
| 313 | doc = "The libraries used by rustc during compilation.", |
| 314 | cfg = "exec", |
| 315 | ), |
| 316 | "rustfmt": attr.label( |
| 317 | doc = "The location of the `rustfmt` binary. Can be a direct source or a filegroup containing one item.", |
| 318 | allow_single_file = True, |
| 319 | cfg = "exec", |
| 320 | mandatory = True, |
| 321 | ), |
| 322 | }, |
| 323 | toolchains = [ |
| 324 | str(Label("@rules_rust//rust:toolchain_type")), |
| 325 | ], |
| 326 | ) |
| 327 | |
| 328 | def _current_rustfmt_toolchain_impl(ctx): |
| 329 | toolchain = ctx.toolchains[str(Label("@rules_rust//rust/rustfmt:toolchain_type"))] |
| 330 | |
| 331 | return [ |
| 332 | toolchain, |
| 333 | toolchain.make_variables, |
| 334 | DefaultInfo( |
| 335 | files = depset([ |
| 336 | toolchain.rustfmt, |
| 337 | ]), |
| 338 | runfiles = ctx.runfiles(transitive_files = toolchain.all_files), |
| 339 | ), |
| 340 | ] |
| 341 | |
| 342 | current_rustfmt_toolchain = rule( |
| 343 | doc = "A rule for exposing the current registered `rustfmt_toolchain`.", |
| 344 | implementation = _current_rustfmt_toolchain_impl, |
| 345 | toolchains = [ |
| 346 | str(Label("@rules_rust//rust/rustfmt:toolchain_type")), |
| 347 | ], |
| 348 | incompatible_use_toolchain_transition = True, |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 349 | ) |