blob: 3b2af85ec16ba18546ed73bdb7400b376c2374e9 [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.
Adam Snaider1c095c92023-07-08 02:09:58 -040078 // Except for targets tagged with `norustfmt`, `no-rustfmt`, or `no-format`.
Brian Silverman5f6f2762022-08-13 19:30:05 -070079 // And except for targets with a populated `crate` attribute since `crate` defines edition for this target
80 format!(
Adam Snaider1c095c92023-07-08 02:09:58 -040081 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) except attr(crate, ".*", $scope), 1)))"#,
Brian Silverman5f6f2762022-08-13 19:30:05 -070082 ),
83 "--keep_going".to_owned(),
84 "--noimplicit_deps".to_owned(),
85 ];
86
87 bazel_command(bazel_bin, &query_args, current_dir)
88}
89
90/// Perform a `bazel` query to determine all source files which are to be
91/// formatted for particular Rust editions.
92fn query_rustfmt_targets(options: &Config) -> HashMap<String, Vec<String>> {
93 let scope = options
94 .packages
95 .clone()
96 .into_iter()
97 .reduce(|acc, item| acc + " " + &item)
98 .unwrap_or_else(|| "//...:all".to_owned());
99
100 let editions = get_editions();
101 let default_edition = get_default_edition();
102
103 editions
104 .into_iter()
105 .map(|edition| {
106 let mut targets = edition_query(&options.bazel, &edition, &scope, &options.workspace);
107
108 // For all targets relying on the toolchain for it's edition,
109 // query anything with an unset edition
110 if edition == default_edition {
111 targets.extend(edition_query(
112 &options.bazel,
113 EMPTY_EDITION,
114 &scope,
115 &options.workspace,
116 ))
117 }
118
119 (edition, targets)
120 })
121 .collect()
122}
123
124/// Run rustfmt on a set of Bazel targets
125fn apply_rustfmt(options: &Config, editions_and_targets: &HashMap<String, Vec<String>>) {
126 // There is no work to do if the list of targets is empty
127 if editions_and_targets.is_empty() {
128 return;
129 }
130
131 for (edition, targets) in editions_and_targets.iter() {
132 if targets.is_empty() {
133 continue;
134 }
135
136 // Get paths to all formattable sources
137 let sources: Vec<String> = targets
138 .iter()
139 .map(|target| target.replace(':', "/").trim_start_matches('/').to_owned())
140 .collect();
141
142 // Run rustfmt
143 let status = Command::new(&options.rustfmt_config.rustfmt)
144 .current_dir(&options.workspace)
145 .arg("--edition")
146 .arg(edition)
147 .arg("--config-path")
148 .arg(&options.rustfmt_config.config)
149 .args(sources)
150 .status()
151 .expect("Failed to run rustfmt");
152
153 if !status.success() {
154 std::process::exit(status.code().unwrap_or(1));
155 }
156 }
157}
158
159/// A struct containing details used for executing rustfmt.
160#[derive(Debug)]
161struct Config {
162 /// The path of the Bazel workspace root.
163 pub workspace: PathBuf,
164
165 /// The Bazel executable to use for builds and queries.
166 pub bazel: PathBuf,
167
168 /// Information about the current rustfmt binary to run.
169 pub rustfmt_config: rustfmt_lib::RustfmtConfig,
170
171 /// Optionally, users can pass a list of targets/packages/scopes
172 /// (eg `//my:target` or `//my/pkg/...`) to control the targets
173 /// to be formatted. If empty, all targets in the workspace will
174 /// be formatted.
175 pub packages: Vec<String>,
176}
177
178/// Parse command line arguments and environment variables to
179/// produce config data for running rustfmt.
180fn parse_args() -> Config {
181 Config{
182 workspace: PathBuf::from(
183 env::var("BUILD_WORKSPACE_DIRECTORY")
184 .expect("The environment variable BUILD_WORKSPACE_DIRECTORY is required for finding the workspace root")
185 ),
186 bazel: PathBuf::from(
187 env::var("BAZEL_REAL")
188 .unwrap_or_else(|_| "bazel".to_owned())
189 ),
190 rustfmt_config: rustfmt_lib::parse_rustfmt_config(),
191 packages: env::args().skip(1).collect(),
192 }
193}