bootstrap/core/build_steps/
format.rs1use std::collections::VecDeque;
4use std::path::{Path, PathBuf};
5use std::process::Command;
6use std::sync::Mutex;
7use std::sync::mpsc::SyncSender;
8
9use build_helper::git::get_git_modified_files;
10use ignore::WalkBuilder;
11
12use crate::core::builder::{Builder, Kind, Step};
13use crate::core::download::maybe_download_rustfmt;
14use crate::utils::build_stamp::BuildStamp;
15use crate::utils::exec::command;
16use crate::utils::helpers::{self, t};
17
18#[must_use]
19enum RustfmtStatus {
20 InProgress,
21 Ok,
22 Failed,
23}
24
25fn rustfmt(
26 src: &Path,
27 rustfmt: &Path,
28 paths: &[PathBuf],
29 check: bool,
30) -> impl FnMut(bool) -> RustfmtStatus + use<> {
31 let mut cmd = Command::new(rustfmt);
32 cmd.arg("--config-path").arg(src.canonicalize().unwrap());
35 cmd.arg("--edition").arg("2024");
36 cmd.arg("--unstable-features");
37 cmd.arg("--skip-children");
38 if check {
39 cmd.arg("--check");
40 }
41 cmd.args(paths);
42 let mut cmd = cmd.spawn().expect("running rustfmt");
43 move |block: bool| -> RustfmtStatus {
46 let status = if !block {
47 match cmd.try_wait() {
48 Ok(Some(status)) => Ok(status),
49 Ok(None) => return RustfmtStatus::InProgress,
50 Err(err) => Err(err),
51 }
52 } else {
53 cmd.wait()
54 };
55 if status.unwrap().success() { RustfmtStatus::Ok } else { RustfmtStatus::Failed }
56 }
57}
58
59fn get_rustfmt_version(builder: &Builder<'_>) -> Option<(String, BuildStamp)> {
60 let stamp_file = BuildStamp::new(&builder.out).with_prefix("rustfmt");
61
62 let rustfmt = builder.ensure(InternalRustfmt);
63 let mut cmd = command(rustfmt.as_ref()?);
64 cmd.arg("--version");
65
66 let output = cmd.allow_failure().run_capture(builder);
67 if output.is_failure() {
68 return None;
69 }
70 Some((output.stdout(), stamp_file))
71}
72
73fn verify_rustfmt_version(builder: &Builder<'_>) -> bool {
75 let Some((version, stamp_file)) = get_rustfmt_version(builder) else {
76 return false;
77 };
78 stamp_file.add_stamp(version).is_up_to_date()
79}
80
81fn update_rustfmt_version(builder: &Builder<'_>) {
83 let Some((version, stamp_file)) = get_rustfmt_version(builder) else {
84 return;
85 };
86
87 t!(stamp_file.add_stamp(version).write());
88}
89
90fn get_modified_rs_files(builder: &Builder<'_>) -> Result<Option<Vec<String>>, String> {
95 assert!(!builder.config.is_running_on_ci());
98
99 if !verify_rustfmt_version(builder) {
100 return Ok(None);
101 }
102
103 get_git_modified_files(&builder.config.git_config(), Some(&builder.config.src), &["rs"])
104 .map(Some)
105}
106
107#[derive(Clone, Debug, PartialEq, Eq, Hash)]
111pub struct InternalRustfmt;
112
113impl Step for InternalRustfmt {
114 type Output = Option<PathBuf>;
115
116 fn run(self, builder: &Builder<'_>) -> Self::Output {
117 if let Some(initial_rustfmt) = &builder.config.external_rustfmt {
119 return Some(initial_rustfmt.clone());
120 }
121 maybe_download_rustfmt(&builder.config, &builder.config.out)
123 }
124}
125
126#[derive(serde_derive::Deserialize)]
127struct RustfmtConfig {
128 ignore: Vec<String>,
129}
130
131fn print_paths(verb: &str, adjective: Option<&str>, paths: &[String]) {
134 let len = paths.len();
135 let adjective =
136 if let Some(adjective) = adjective { format!("{adjective} ") } else { String::new() };
137 if len <= 10 {
138 for path in paths {
139 println!("fmt: {verb} {adjective}file {path}");
140 }
141 } else {
142 println!("fmt: {verb} {len} {adjective}files");
143 }
144}
145
146pub fn format(
147 builder: &Builder<'_>,
148 rustfmt_path: PathBuf,
149 check: bool,
150 all: bool,
151 paths: &[PathBuf],
152) {
153 if builder.kind == Kind::Format && builder.top_stage != 0 {
154 eprintln!("ERROR: `x fmt` only supports stage 0.");
155 eprintln!("HELP: Use `x run rustfmt` to run in-tree rustfmt.");
156 helpers::exit_process(1);
157 }
158
159 if !paths.is_empty() {
160 eprintln!(
161 "fmt error: path arguments are no longer accepted; use `--all` to format everything"
162 );
163 helpers::exit_process(1);
164 };
165 if builder.config.dry_run() {
166 return;
167 }
168
169 let all = all || builder.config.is_running_on_ci();
174
175 let matcher = {
176 let mut types = ignore::types::TypesBuilder::new();
177 types.add_defaults();
178 types.select("rust");
179 types.build().unwrap()
180 };
181 let rustfmt_config = builder.src.join("rustfmt.toml");
182 if !rustfmt_config.exists() {
183 eprintln!("fmt error: Not running formatting checks; rustfmt.toml does not exist.");
184 eprintln!("fmt error: This may happen in distributed tarballs.");
185 return;
186 }
187 let rustfmt_config = t!(std::fs::read_to_string(&rustfmt_config));
188 let rustfmt_config: RustfmtConfig = t!(toml::from_str(&rustfmt_config));
189 let mut override_builder = ignore::overrides::OverrideBuilder::new(&builder.src);
190 for ignore in rustfmt_config.ignore {
191 if ignore.starts_with('!') {
192 eprintln!("fmt error: `!`-prefixed entries are not supported in rustfmt.toml, sorry");
199 helpers::exit_process(1);
200 } else {
201 override_builder.add(&format!("!{ignore}")).expect(&ignore);
202 }
203 }
204 let git_available =
205 helpers::git(None).allow_failure().arg("--version").run_capture(builder).is_success();
206
207 let mut adjective = None;
208 if git_available {
209 let in_working_tree = helpers::git(Some(&builder.src))
210 .allow_failure()
211 .arg("rev-parse")
212 .arg("--is-inside-work-tree")
213 .run_capture(builder)
214 .is_success();
215 if in_working_tree {
216 let untracked_paths_output = helpers::git(Some(&builder.src))
217 .arg("status")
218 .arg("--porcelain")
219 .arg("-z")
220 .arg("--untracked-files=normal")
221 .run_capture_stdout(builder)
222 .stdout();
223 let untracked_paths: Vec<_> = untracked_paths_output
224 .split_terminator('\0')
225 .filter_map(
226 |entry| entry.strip_prefix("?? "), )
228 .map(|x| x.to_string())
229 .collect();
230 print_paths("skipped", Some("untracked"), &untracked_paths);
231
232 for untracked_path in untracked_paths {
233 override_builder.add(&format!("!/{untracked_path}")).expect(&untracked_path);
239 }
240 if !all {
241 adjective = Some("modified");
242 match get_modified_rs_files(builder) {
243 Ok(Some(files)) => {
244 if files.is_empty() {
245 println!("fmt info: No modified files detected for formatting.");
246 return;
247 }
248
249 for file in files {
250 override_builder.add(&format!("/{file}")).expect(&file);
251 }
252 }
253 Ok(None) => {
254 }
260 Err(err) => {
261 eprintln!("fmt warning: Something went wrong running git commands:");
262 eprintln!("fmt warning: {err}");
263 eprintln!("fmt warning: Falling back to formatting all files.");
264 }
265 }
266 }
267 } else {
268 eprintln!("fmt: warning: Not in git tree. Skipping git-aware format checks");
269 }
270 } else {
271 eprintln!("fmt: warning: Could not find usable git. Skipping git-aware format checks");
272 }
273
274 let override_ = override_builder.build().unwrap(); assert!(rustfmt_path.exists(), "{}", rustfmt_path.display());
277 let src = builder.src.clone();
278 let (tx, rx): (SyncSender<PathBuf>, _) = std::sync::mpsc::sync_channel(128);
279 let walker = WalkBuilder::new(src.clone()).types(matcher).overrides(override_).build_parallel();
280
281 let max_processes = builder.jobs() as usize * 2;
284
285 let thread = std::thread::spawn(move || {
288 let mut result = Ok(());
289
290 let mut children = VecDeque::new();
291 while let Ok(path) = rx.recv() {
292 let paths: Vec<_> = rx.try_iter().take(63).chain(std::iter::once(path)).collect();
295
296 let child = rustfmt(&src, &rustfmt_path, paths.as_slice(), check);
297 children.push_back(child);
298
299 for i in (0..children.len()).rev() {
301 match children[i](false) {
302 RustfmtStatus::InProgress => {}
303 RustfmtStatus::Failed => {
304 result = Err(());
305 children.swap_remove_back(i);
306 break;
307 }
308 RustfmtStatus::Ok => {
309 children.swap_remove_back(i);
310 break;
311 }
312 }
313 }
314
315 if children.len() >= max_processes {
316 match children.pop_front().unwrap()(true) {
318 RustfmtStatus::InProgress | RustfmtStatus::Ok => {}
319 RustfmtStatus::Failed => result = Err(()),
320 }
321 }
322 }
323
324 for mut child in children {
326 match child(true) {
327 RustfmtStatus::InProgress | RustfmtStatus::Ok => {}
328 RustfmtStatus::Failed => result = Err(()),
329 }
330 }
331
332 result
333 });
334
335 let formatted_paths = Mutex::new(Vec::new());
336 let formatted_paths_ref = &formatted_paths;
337 walker.run(|| {
338 let tx = tx.clone();
339 Box::new(move |entry| {
340 let cwd = std::env::current_dir();
341 let entry = t!(entry);
342 if entry.file_type().is_some_and(|t| t.is_file()) {
343 formatted_paths_ref.lock().unwrap().push({
344 let mut path = entry.clone().into_path();
347 if let Ok(cwd) = cwd
348 && let Ok(path2) = path.strip_prefix(cwd)
349 {
350 path = path2.to_path_buf();
351 }
352 path.display().to_string()
353 });
354 t!(tx.send(entry.into_path()));
355 }
356 ignore::WalkState::Continue
357 })
358 });
359 let mut paths = formatted_paths.into_inner().unwrap();
360 paths.sort();
361 print_paths(if check { "checked" } else { "formatted" }, adjective, &paths);
362
363 drop(tx);
364
365 let result = thread.join().unwrap();
366
367 if result.is_err() {
368 helpers::exit_process(1);
369 }
370
371 update_rustfmt_version(builder);
377}