blob: 4e4419f9d7b332b8fa0fdc1a510bc256b2119cfa [file] [log] [blame]
Brian Silvermancc09f182022-03-09 15:40:20 -08001use std::env;
2use std::path::PathBuf;
3use std::process::{Command, Stdio};
4use std::str;
5
6fn main() {
7 // Gather all command line and environment settings
8 let options = parse_args();
9
10 // Gather a list of all formattable targets
11 let targets = query_rustfmt_targets(&options);
12
13 // Run rustfmt on these targets
14 apply_rustfmt(&options, &targets);
15}
16
17/// Perform a `bazel` query to determine a list of Bazel targets which are to be formatted.
18fn query_rustfmt_targets(options: &Config) -> Vec<String> {
19 // Determine what packages to query
20 let scope = match options.packages.is_empty() {
21 true => "//...:all".to_owned(),
22 false => {
23 // Check to see if all the provided packages are actually targets
24 let is_all_targets = options
25 .packages
26 .iter()
27 .all(|pkg| match label::analyze(pkg) {
28 Ok(tgt) => tgt.name != "all",
29 Err(_) => false,
30 });
31
32 // Early return if a list of targets and not packages were provided
33 if is_all_targets {
34 return options.packages.clone();
35 }
36
37 options.packages.join(" + ")
38 }
39 };
40
41 let query_args = vec![
42 "query".to_owned(),
43 format!(
44 r#"kind('{types}', {scope}) except attr(tags, 'norustfmt', kind('{types}', {scope}))"#,
45 types = "^rust_",
46 scope = scope
47 ),
48 ];
49
50 let child = Command::new(&options.bazel)
51 .current_dir(&options.workspace)
52 .args(query_args)
53 .stdout(Stdio::piped())
54 .stderr(Stdio::inherit())
55 .spawn()
56 .expect("Failed to spawn bazel query command");
57
58 let output = child
59 .wait_with_output()
60 .expect("Failed to wait on spawned command");
61
62 if !output.status.success() {
63 std::process::exit(output.status.code().unwrap_or(1));
64 }
65
66 str::from_utf8(&output.stdout)
67 .expect("Invalid stream from command")
68 .split('\n')
69 .filter(|line| !line.is_empty())
70 .map(|line| line.to_string())
71 .collect()
72}
73
74/// Build a list of Bazel targets using the `rustfmt_aspect` to produce the
75/// arguments to use when formatting the sources of those targets.
76fn generate_rustfmt_target_manifests(options: &Config, targets: &[String]) {
77 let build_args = vec![
78 "build".to_owned(),
79 format!(
80 "--aspects={}//rust:defs.bzl%rustfmt_aspect",
81 env!("ASPECT_REPOSITORY")
82 ),
83 "--output_groups=rustfmt_manifest".to_owned(),
84 ];
85
86 let child = Command::new(&options.bazel)
87 .current_dir(&options.workspace)
88 .args(build_args)
89 .args(targets)
90 .stdout(Stdio::piped())
91 .stderr(Stdio::inherit())
92 .spawn()
93 .expect("Failed to spawn command");
94
95 let output = child
96 .wait_with_output()
97 .expect("Failed to wait on spawned command");
98
99 if !output.status.success() {
100 std::process::exit(output.status.code().unwrap_or(1));
101 }
102}
103
104/// Run rustfmt on a set of Bazel targets
105fn apply_rustfmt(options: &Config, targets: &[String]) {
106 // Ensure the targets are first built and a manifest containing `rustfmt`
107 // arguments are generated before formatting source files.
108 generate_rustfmt_target_manifests(options, targets);
109
110 for target in targets.iter() {
111 // Replace any `:` characters and strip leading slashes
112 let target_path = target.replace(':', "/").trim_start_matches('/').to_owned();
113
114 // Find a manifest for the current target. Not all targets will have one
115 let manifest = options.workspace.join("bazel-bin").join(format!(
116 "{}.{}",
117 &target_path,
118 rustfmt_lib::RUSTFMT_MANIFEST_EXTENSION,
119 ));
120
121 if !manifest.exists() {
122 continue;
123 }
124
125 // Load the manifest containing rustfmt arguments
126 let rustfmt_config = rustfmt_lib::parse_rustfmt_manifest(&manifest);
127
128 // Ignore any targets which do not have source files. This can
129 // occur in cases where all source files are generated.
130 if rustfmt_config.sources.is_empty() {
131 continue;
132 }
133
134 // Run rustfmt
135 let status = Command::new(&options.rustfmt_config.rustfmt)
136 .current_dir(&options.workspace)
137 .arg("--edition")
138 .arg(rustfmt_config.edition)
139 .arg("--config-path")
140 .arg(&options.rustfmt_config.config)
141 .args(rustfmt_config.sources)
142 .status()
143 .expect("Failed to run rustfmt");
144
145 if !status.success() {
146 std::process::exit(status.code().unwrap_or(1));
147 }
148 }
149}
150
151/// A struct containing details used for executing rustfmt.
152#[derive(Debug)]
153struct Config {
154 /// The path of the Bazel workspace root.
155 pub workspace: PathBuf,
156
157 /// The Bazel executable to use for builds and queries.
158 pub bazel: PathBuf,
159
160 /// Information about the current rustfmt binary to run.
161 pub rustfmt_config: rustfmt_lib::RustfmtConfig,
162
163 /// Optionally, users can pass a list of targets/packages/scopes
164 /// (eg `//my:target` or `//my/pkg/...`) to control the targets
165 /// to be formatted. If empty, all targets in the workspace will
166 /// be formatted.
167 pub packages: Vec<String>,
168}
169
170/// Parse command line arguments and environment variables to
171/// produce config data for running rustfmt.
172fn parse_args() -> Config {
173 Config{
174 workspace: PathBuf::from(
175 env::var("BUILD_WORKSPACE_DIRECTORY")
176 .expect("The environment variable BUILD_WORKSPACE_DIRECTORY is required for finding the workspace root")
177 ),
178 bazel: PathBuf::from(
179 env::var("BAZEL_REAL")
180 .unwrap_or_else(|_| "bazel".to_owned())
181 ),
182 rustfmt_config: rustfmt_lib::parse_rustfmt_config(),
183 packages: env::args().skip(1).collect(),
184 }
185}