xref: /aosp_15_r20/external/bazelbuild-rules_rust/tools/rustfmt/src/main.rs (revision d4726bddaa87cc4778e7472feed243fa4b6c267f)
1 //! A tool for querying Rust source files wired into Bazel and running Rustfmt on them.
2 
3 use std::collections::HashMap;
4 use std::env;
5 use std::path::{Path, PathBuf};
6 use std::process::{Command, Stdio};
7 use std::str;
8 
9 /// The Bazel Rustfmt tool entry point
main()10 fn 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
22 const 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.
get_default_edition() -> &'static str26 fn 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
get_editions() -> Vec<String>35 fn 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
bazel_command(bazel_bin: &Path, args: &[String], current_dir: &Path) -> Vec<String>40 fn 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
67 const EMPTY_EDITION: &str = "^$";
68 
69 /// 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 fn 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`, `no-rustfmt`, or `no-format`.
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) except attr(crate, ".*", $scope), 1)))"#,
82         ),
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.
query_rustfmt_targets(options: &Config) -> HashMap<String, Vec<String>>92 fn 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
apply_rustfmt(options: &Config, editions_and_targets: &HashMap<String, Vec<String>>)125 fn 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)]
161 struct 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.
parse_args() -> Config180 fn 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 }
194