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(build: &Builder<'_>) -> Option<(String, BuildStamp)> {
60 let stamp_file = BuildStamp::new(&build.out).with_prefix("rustfmt");
61
62 let rustfmt = build.ensure(InternalRustfmt);
63 let mut cmd = command(rustfmt.as_ref()?);
64 cmd.arg("--version");
65
66 let output = cmd.allow_failure().run_capture(build);
67 if output.is_failure() {
68 return None;
69 }
70 Some((output.stdout(), stamp_file))
71}
72
73fn verify_rustfmt_version(build: &Builder<'_>) -> bool {
75 let Some((version, stamp_file)) = get_rustfmt_version(build) else {
76 return false;
77 };
78 stamp_file.add_stamp(version).is_up_to_date()
79}
80
81fn update_rustfmt_version(build: &Builder<'_>) {
83 let Some((version, stamp_file)) = get_rustfmt_version(build) else {
84 return;
85 };
86
87 t!(stamp_file.add_stamp(version).write());
88}
89
90fn get_modified_rs_files(build: &Builder<'_>) -> Result<Option<Vec<String>>, String> {
95 assert!(!build.config.is_running_on_ci());
98
99 if !verify_rustfmt_version(build) {
100 return Ok(None);
101 }
102
103 get_git_modified_files(&build.config.git_config(), Some(&build.config.src), &["rs"]).map(Some)
104}
105
106#[derive(Clone, Debug, PartialEq, Eq, Hash)]
110pub struct InternalRustfmt;
111
112impl Step for InternalRustfmt {
113 type Output = Option<PathBuf>;
114
115 fn run(self, builder: &Builder<'_>) -> Self::Output {
116 if let Some(initial_rustfmt) = &builder.config.external_rustfmt {
118 return Some(initial_rustfmt.clone());
119 }
120 maybe_download_rustfmt(&builder.config, &builder.config.out)
122 }
123}
124
125#[derive(serde_derive::Deserialize)]
126struct RustfmtConfig {
127 ignore: Vec<String>,
128}
129
130fn print_paths(verb: &str, adjective: Option<&str>, paths: &[String]) {
133 let len = paths.len();
134 let adjective =
135 if let Some(adjective) = adjective { format!("{adjective} ") } else { String::new() };
136 if len <= 10 {
137 for path in paths {
138 println!("fmt: {verb} {adjective}file {path}");
139 }
140 } else {
141 println!("fmt: {verb} {len} {adjective}files");
142 }
143}
144
145pub fn format(
146 build: &Builder<'_>,
147 rustfmt_path: PathBuf,
148 check: bool,
149 all: bool,
150 paths: &[PathBuf],
151) {
152 if build.kind == Kind::Format && build.top_stage != 0 {
153 eprintln!("ERROR: `x fmt` only supports stage 0.");
154 eprintln!("HELP: Use `x run rustfmt` to run in-tree rustfmt.");
155 crate::exit!(1);
156 }
157
158 if !paths.is_empty() {
159 eprintln!(
160 "fmt error: path arguments are no longer accepted; use `--all` to format everything"
161 );
162 crate::exit!(1);
163 };
164 if build.config.dry_run() {
165 return;
166 }
167
168 let all = all || build.config.is_running_on_ci();
173
174 let mut builder = ignore::types::TypesBuilder::new();
175 builder.add_defaults();
176 builder.select("rust");
177 let matcher = builder.build().unwrap();
178 let rustfmt_config = build.src.join("rustfmt.toml");
179 if !rustfmt_config.exists() {
180 eprintln!("fmt error: Not running formatting checks; rustfmt.toml does not exist.");
181 eprintln!("fmt error: This may happen in distributed tarballs.");
182 return;
183 }
184 let rustfmt_config = t!(std::fs::read_to_string(&rustfmt_config));
185 let rustfmt_config: RustfmtConfig = t!(toml::from_str(&rustfmt_config));
186 let mut override_builder = ignore::overrides::OverrideBuilder::new(&build.src);
187 for ignore in rustfmt_config.ignore {
188 if ignore.starts_with('!') {
189 eprintln!("fmt error: `!`-prefixed entries are not supported in rustfmt.toml, sorry");
196 crate::exit!(1);
197 } else {
198 override_builder.add(&format!("!{ignore}")).expect(&ignore);
199 }
200 }
201 let git_available =
202 helpers::git(None).allow_failure().arg("--version").run_capture(build).is_success();
203
204 let mut adjective = None;
205 if git_available {
206 let in_working_tree = helpers::git(Some(&build.src))
207 .allow_failure()
208 .arg("rev-parse")
209 .arg("--is-inside-work-tree")
210 .run_capture(build)
211 .is_success();
212 if in_working_tree {
213 let untracked_paths_output = helpers::git(Some(&build.src))
214 .arg("status")
215 .arg("--porcelain")
216 .arg("-z")
217 .arg("--untracked-files=normal")
218 .run_capture_stdout(build)
219 .stdout();
220 let untracked_paths: Vec<_> = untracked_paths_output
221 .split_terminator('\0')
222 .filter_map(
223 |entry| entry.strip_prefix("?? "), )
225 .map(|x| x.to_string())
226 .collect();
227 print_paths("skipped", Some("untracked"), &untracked_paths);
228
229 for untracked_path in untracked_paths {
230 override_builder.add(&format!("!/{untracked_path}")).expect(&untracked_path);
236 }
237 if !all {
238 adjective = Some("modified");
239 match get_modified_rs_files(build) {
240 Ok(Some(files)) => {
241 if files.is_empty() {
242 println!("fmt info: No modified files detected for formatting.");
243 return;
244 }
245
246 for file in files {
247 override_builder.add(&format!("/{file}")).expect(&file);
248 }
249 }
250 Ok(None) => {
251 }
257 Err(err) => {
258 eprintln!("fmt warning: Something went wrong running git commands:");
259 eprintln!("fmt warning: {err}");
260 eprintln!("fmt warning: Falling back to formatting all files.");
261 }
262 }
263 }
264 } else {
265 eprintln!("fmt: warning: Not in git tree. Skipping git-aware format checks");
266 }
267 } else {
268 eprintln!("fmt: warning: Could not find usable git. Skipping git-aware format checks");
269 }
270
271 let override_ = override_builder.build().unwrap(); assert!(rustfmt_path.exists(), "{}", rustfmt_path.display());
274 let src = build.src.clone();
275 let (tx, rx): (SyncSender<PathBuf>, _) = std::sync::mpsc::sync_channel(128);
276 let walker = WalkBuilder::new(src.clone()).types(matcher).overrides(override_).build_parallel();
277
278 let max_processes = build.jobs() as usize * 2;
281
282 let thread = std::thread::spawn(move || {
285 let mut result = Ok(());
286
287 let mut children = VecDeque::new();
288 while let Ok(path) = rx.recv() {
289 let paths: Vec<_> = rx.try_iter().take(63).chain(std::iter::once(path)).collect();
292
293 let child = rustfmt(&src, &rustfmt_path, paths.as_slice(), check);
294 children.push_back(child);
295
296 for i in (0..children.len()).rev() {
298 match children[i](false) {
299 RustfmtStatus::InProgress => {}
300 RustfmtStatus::Failed => {
301 result = Err(());
302 children.swap_remove_back(i);
303 break;
304 }
305 RustfmtStatus::Ok => {
306 children.swap_remove_back(i);
307 break;
308 }
309 }
310 }
311
312 if children.len() >= max_processes {
313 match children.pop_front().unwrap()(true) {
315 RustfmtStatus::InProgress | RustfmtStatus::Ok => {}
316 RustfmtStatus::Failed => result = Err(()),
317 }
318 }
319 }
320
321 for mut child in children {
323 match child(true) {
324 RustfmtStatus::InProgress | RustfmtStatus::Ok => {}
325 RustfmtStatus::Failed => result = Err(()),
326 }
327 }
328
329 result
330 });
331
332 let formatted_paths = Mutex::new(Vec::new());
333 let formatted_paths_ref = &formatted_paths;
334 walker.run(|| {
335 let tx = tx.clone();
336 Box::new(move |entry| {
337 let cwd = std::env::current_dir();
338 let entry = t!(entry);
339 if entry.file_type().is_some_and(|t| t.is_file()) {
340 formatted_paths_ref.lock().unwrap().push({
341 let mut path = entry.clone().into_path();
344 if let Ok(cwd) = cwd
345 && let Ok(path2) = path.strip_prefix(cwd)
346 {
347 path = path2.to_path_buf();
348 }
349 path.display().to_string()
350 });
351 t!(tx.send(entry.into_path()));
352 }
353 ignore::WalkState::Continue
354 })
355 });
356 let mut paths = formatted_paths.into_inner().unwrap();
357 paths.sort();
358 print_paths(if check { "checked" } else { "formatted" }, adjective, &paths);
359
360 drop(tx);
361
362 let result = thread.join().unwrap();
363
364 if result.is_err() {
365 crate::exit!(1);
366 }
367
368 update_rustfmt_version(build);
374}