blob: 5ca10f88ea8d6eda76b52f55bc2c6ea3c2589ee9 [file] [log] [blame]
Brian Silverman5f6f2762022-08-13 19:30:05 -07001//! A tool for querying Rust source files wired into Bazel and running Rustfmt on them.
2
3use std::collections::HashMap;
4use std::env;
5use std::path::{Path, PathBuf};
6use std::process::{Command, Stdio};
7use std::str;
8
9/// The Bazel Rustfmt tool entry point
10fn main() {
11 // Gather all command line and environment settings
12 let options = parse_args();
13
14 // Gather a list of all formattable targets
15 let targets = query_rustfmt_targets(&options);
16
17 // Run rustfmt on these targets
18 apply_rustfmt(&options, &targets);
19}
20
21/// The edition to use in cases where the default edition is unspecified by Bazel
22const FALLBACK_EDITION: &str = "2018";
23
24/// Determine the Rust edition to use in cases where a target has not explicitly
25/// specified the edition via an `edition` attribute.
26fn get_default_edition() -> &'static str {
27 if !env!("RUST_DEFAULT_EDITION").is_empty() {
28 env!("RUST_DEFAULT_EDITION")
29 } else {
30 FALLBACK_EDITION
31 }
32}
33
34/// Get a list of all editions to run formatting for
35fn get_editions() -> Vec<String> {
36 vec!["2015".to_owned(), "2018".to_owned(), "2021".to_owned()]
37}
38
39/// Run a bazel command, capturing stdout while streaming stderr to surface errors
40fn bazel_command(bazel_bin: &Path, args: &[String], current_dir: &Path) -> Vec<String> {
41 let child = Command::new(bazel_bin)
42 .current_dir(current_dir)
43 .args(args)
44 .stdout(Stdio::piped())
45 .stderr(Stdio::inherit())
46 .spawn()
47 .expect("Failed to spawn bazel command");
48
49 let output = child
50 .wait_with_output()
51 .expect("Failed to wait on spawned command");
52
53 if !output.status.success() {
54 eprintln!("Failed to perform `bazel query` command.");
55 std::process::exit(output.status.code().unwrap_or(1));
56 }
57
58 str::from_utf8(&output.stdout)
59 .expect("Invalid stream from command")
60 .split('\n')
61 .filter(|line| !line.is_empty())
62 .map(|line| line.to_string())
63 .collect()
64}
65
66/// The regex representation of an empty `edition` attribute
67const EMPTY_EDITION: &str = "^$";
68
69/// Query for all `*.rs` files in a workspace that are dependencies of targets with the requested edition.
70fn edition_query(bazel_bin: &Path, edition: &str, scope: &str, current_dir: &Path) -> Vec<String> {
71 let query_args = vec![
72 "query".to_owned(),
73 // Query explanation:
74 // Filter all local targets ending in `*.rs`.
75 // Get all source files.
76 // Get direct dependencies.
77 // Get all targets with the specified `edition` attribute.
78 // Except for targets tagged with `norustfmt`.
79 // And except for targets with a populated `crate` attribute since `crate` defines edition for this target
80 format!(
81 r#"let scope = set({scope}) in filter("^//.*\.rs$", kind("source file", deps(attr(edition, "{edition}", $scope) except attr(tags, "(^\[|, )(no-format|no-rustfmt|norustfmt)(, |\]$)", $scope) + attr(crate, ".*", $scope), 1)))"#,
82 edition = edition,
83 scope = scope,
84 ),
85 "--keep_going".to_owned(),
86 "--noimplicit_deps".to_owned(),
87 ];
88
89 bazel_command(bazel_bin, &query_args, current_dir)
90}
91
92/// Perform a `bazel` query to determine all source files which are to be
93/// formatted for particular Rust editions.
94fn query_rustfmt_targets(options: &Config) -> HashMap<String, Vec<String>> {
95 let scope = options
96 .packages
97 .clone()
98 .into_iter()
99 .reduce(|acc, item| acc + " " + &item)
100 .unwrap_or_else(|| "//...:all".to_owned());
101
102 let editions = get_editions();
103 let default_edition = get_default_edition();
104
105 editions
106 .into_iter()
107 .map(|edition| {
108 let mut targets = edition_query(&options.bazel, &edition, &scope, &options.workspace);
109
110 // For all targets relying on the toolchain for it's edition,
111 // query anything with an unset edition
112 if edition == default_edition {
113 targets.extend(edition_query(
114 &options.bazel,
115 EMPTY_EDITION,
116 &scope,
117 &options.workspace,
118 ))
119 }
120
121 (edition, targets)
122 })
123 .collect()
124}
125
126/// Run rustfmt on a set of Bazel targets
127fn apply_rustfmt(options: &Config, editions_and_targets: &HashMap<String, Vec<String>>) {
128 // There is no work to do if the list of targets is empty
129 if editions_and_targets.is_empty() {
130 return;
131 }
132
133 for (edition, targets) in editions_and_targets.iter() {
134 if targets.is_empty() {
135 continue;
136 }
137
138 // Get paths to all formattable sources
139 let sources: Vec<String> = targets
140 .iter()
141 .map(|target| target.replace(':', "/").trim_start_matches('/').to_owned())
142 .collect();
143
144 // Run rustfmt
145 let status = Command::new(&options.rustfmt_config.rustfmt)
146 .current_dir(&options.workspace)
147 .arg("--edition")
148 .arg(edition)
149 .arg("--config-path")
150 .arg(&options.rustfmt_config.config)
151 .args(sources)
152 .status()
153 .expect("Failed to run rustfmt");
154
155 if !status.success() {
156 std::process::exit(status.code().unwrap_or(1));
157 }
158 }
159}
160
161/// A struct containing details used for executing rustfmt.
162#[derive(Debug)]
163struct Config {
164 /// The path of the Bazel workspace root.
165 pub workspace: PathBuf,
166
167 /// The Bazel executable to use for builds and queries.
168 pub bazel: PathBuf,
169
170 /// Information about the current rustfmt binary to run.
171 pub rustfmt_config: rustfmt_lib::RustfmtConfig,
172
173 /// Optionally, users can pass a list of targets/packages/scopes
174 /// (eg `//my:target` or `//my/pkg/...`) to control the targets
175 /// to be formatted. If empty, all targets in the workspace will
176 /// be formatted.
177 pub packages: Vec<String>,
178}
179
180/// Parse command line arguments and environment variables to
181/// produce config data for running rustfmt.
182fn parse_args() -> Config {
183 Config{
184 workspace: PathBuf::from(
185 env::var("BUILD_WORKSPACE_DIRECTORY")
186 .expect("The environment variable BUILD_WORKSPACE_DIRECTORY is required for finding the workspace root")
187 ),
188 bazel: PathBuf::from(
189 env::var("BAZEL_REAL")
190 .unwrap_or_else(|_| "bazel".to_owned())
191 ),
192 rustfmt_config: rustfmt_lib::parse_rustfmt_config(),
193 packages: env::args().skip(1).collect(),
194 }
195}