Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame^] | 1 | use std::env; |
| 2 | use std::path::PathBuf; |
| 3 | use std::process::{Command, Stdio}; |
| 4 | use std::str; |
| 5 | |
| 6 | fn main() { |
| 7 | // Gather all command line and environment settings |
| 8 | let options = parse_args(); |
| 9 | |
| 10 | // Gather a list of all formattable targets |
| 11 | let targets = query_rustfmt_targets(&options); |
| 12 | |
| 13 | // Run rustfmt on these targets |
| 14 | apply_rustfmt(&options, &targets); |
| 15 | } |
| 16 | |
| 17 | /// Perform a `bazel` query to determine a list of Bazel targets which are to be formatted. |
| 18 | fn query_rustfmt_targets(options: &Config) -> Vec<String> { |
| 19 | // Determine what packages to query |
| 20 | let scope = match options.packages.is_empty() { |
| 21 | true => "//...:all".to_owned(), |
| 22 | false => { |
| 23 | // Check to see if all the provided packages are actually targets |
| 24 | let is_all_targets = options |
| 25 | .packages |
| 26 | .iter() |
| 27 | .all(|pkg| match label::analyze(pkg) { |
| 28 | Ok(tgt) => tgt.name != "all", |
| 29 | Err(_) => false, |
| 30 | }); |
| 31 | |
| 32 | // Early return if a list of targets and not packages were provided |
| 33 | if is_all_targets { |
| 34 | return options.packages.clone(); |
| 35 | } |
| 36 | |
| 37 | options.packages.join(" + ") |
| 38 | } |
| 39 | }; |
| 40 | |
| 41 | let query_args = vec![ |
| 42 | "query".to_owned(), |
| 43 | format!( |
| 44 | r#"kind('{types}', {scope}) except attr(tags, 'norustfmt', kind('{types}', {scope}))"#, |
| 45 | types = "^rust_", |
| 46 | scope = scope |
| 47 | ), |
| 48 | ]; |
| 49 | |
| 50 | let child = Command::new(&options.bazel) |
| 51 | .current_dir(&options.workspace) |
| 52 | .args(query_args) |
| 53 | .stdout(Stdio::piped()) |
| 54 | .stderr(Stdio::inherit()) |
| 55 | .spawn() |
| 56 | .expect("Failed to spawn bazel query command"); |
| 57 | |
| 58 | let output = child |
| 59 | .wait_with_output() |
| 60 | .expect("Failed to wait on spawned command"); |
| 61 | |
| 62 | if !output.status.success() { |
| 63 | std::process::exit(output.status.code().unwrap_or(1)); |
| 64 | } |
| 65 | |
| 66 | str::from_utf8(&output.stdout) |
| 67 | .expect("Invalid stream from command") |
| 68 | .split('\n') |
| 69 | .filter(|line| !line.is_empty()) |
| 70 | .map(|line| line.to_string()) |
| 71 | .collect() |
| 72 | } |
| 73 | |
| 74 | /// Build a list of Bazel targets using the `rustfmt_aspect` to produce the |
| 75 | /// arguments to use when formatting the sources of those targets. |
| 76 | fn generate_rustfmt_target_manifests(options: &Config, targets: &[String]) { |
| 77 | let build_args = vec![ |
| 78 | "build".to_owned(), |
| 79 | format!( |
| 80 | "--aspects={}//rust:defs.bzl%rustfmt_aspect", |
| 81 | env!("ASPECT_REPOSITORY") |
| 82 | ), |
| 83 | "--output_groups=rustfmt_manifest".to_owned(), |
| 84 | ]; |
| 85 | |
| 86 | let child = Command::new(&options.bazel) |
| 87 | .current_dir(&options.workspace) |
| 88 | .args(build_args) |
| 89 | .args(targets) |
| 90 | .stdout(Stdio::piped()) |
| 91 | .stderr(Stdio::inherit()) |
| 92 | .spawn() |
| 93 | .expect("Failed to spawn command"); |
| 94 | |
| 95 | let output = child |
| 96 | .wait_with_output() |
| 97 | .expect("Failed to wait on spawned command"); |
| 98 | |
| 99 | if !output.status.success() { |
| 100 | std::process::exit(output.status.code().unwrap_or(1)); |
| 101 | } |
| 102 | } |
| 103 | |
| 104 | /// Run rustfmt on a set of Bazel targets |
| 105 | fn apply_rustfmt(options: &Config, targets: &[String]) { |
| 106 | // Ensure the targets are first built and a manifest containing `rustfmt` |
| 107 | // arguments are generated before formatting source files. |
| 108 | generate_rustfmt_target_manifests(options, targets); |
| 109 | |
| 110 | for target in targets.iter() { |
| 111 | // Replace any `:` characters and strip leading slashes |
| 112 | let target_path = target.replace(':', "/").trim_start_matches('/').to_owned(); |
| 113 | |
| 114 | // Find a manifest for the current target. Not all targets will have one |
| 115 | let manifest = options.workspace.join("bazel-bin").join(format!( |
| 116 | "{}.{}", |
| 117 | &target_path, |
| 118 | rustfmt_lib::RUSTFMT_MANIFEST_EXTENSION, |
| 119 | )); |
| 120 | |
| 121 | if !manifest.exists() { |
| 122 | continue; |
| 123 | } |
| 124 | |
| 125 | // Load the manifest containing rustfmt arguments |
| 126 | let rustfmt_config = rustfmt_lib::parse_rustfmt_manifest(&manifest); |
| 127 | |
| 128 | // Ignore any targets which do not have source files. This can |
| 129 | // occur in cases where all source files are generated. |
| 130 | if rustfmt_config.sources.is_empty() { |
| 131 | continue; |
| 132 | } |
| 133 | |
| 134 | // Run rustfmt |
| 135 | let status = Command::new(&options.rustfmt_config.rustfmt) |
| 136 | .current_dir(&options.workspace) |
| 137 | .arg("--edition") |
| 138 | .arg(rustfmt_config.edition) |
| 139 | .arg("--config-path") |
| 140 | .arg(&options.rustfmt_config.config) |
| 141 | .args(rustfmt_config.sources) |
| 142 | .status() |
| 143 | .expect("Failed to run rustfmt"); |
| 144 | |
| 145 | if !status.success() { |
| 146 | std::process::exit(status.code().unwrap_or(1)); |
| 147 | } |
| 148 | } |
| 149 | } |
| 150 | |
| 151 | /// A struct containing details used for executing rustfmt. |
| 152 | #[derive(Debug)] |
| 153 | struct Config { |
| 154 | /// The path of the Bazel workspace root. |
| 155 | pub workspace: PathBuf, |
| 156 | |
| 157 | /// The Bazel executable to use for builds and queries. |
| 158 | pub bazel: PathBuf, |
| 159 | |
| 160 | /// Information about the current rustfmt binary to run. |
| 161 | pub rustfmt_config: rustfmt_lib::RustfmtConfig, |
| 162 | |
| 163 | /// Optionally, users can pass a list of targets/packages/scopes |
| 164 | /// (eg `//my:target` or `//my/pkg/...`) to control the targets |
| 165 | /// to be formatted. If empty, all targets in the workspace will |
| 166 | /// be formatted. |
| 167 | pub packages: Vec<String>, |
| 168 | } |
| 169 | |
| 170 | /// Parse command line arguments and environment variables to |
| 171 | /// produce config data for running rustfmt. |
| 172 | fn parse_args() -> Config { |
| 173 | Config{ |
| 174 | workspace: PathBuf::from( |
| 175 | env::var("BUILD_WORKSPACE_DIRECTORY") |
| 176 | .expect("The environment variable BUILD_WORKSPACE_DIRECTORY is required for finding the workspace root") |
| 177 | ), |
| 178 | bazel: PathBuf::from( |
| 179 | env::var("BAZEL_REAL") |
| 180 | .unwrap_or_else(|_| "bazel".to_owned()) |
| 181 | ), |
| 182 | rustfmt_config: rustfmt_lib::parse_rustfmt_config(), |
| 183 | packages: env::args().skip(1).collect(), |
| 184 | } |
| 185 | } |