xref: /aosp_15_r20/external/bazelbuild-rules_rust/tools/rustfmt/src/main.rs (revision d4726bddaa87cc4778e7472feed243fa4b6c267f)
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