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