//! Runs rustfmt on the repository. use crate::builder::Builder; use crate::util::{output, program_out_of_date, t}; use build_helper::ci::CiEnv; use build_helper::git::get_git_modified_files; use ignore::WalkBuilder; use std::collections::VecDeque; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; use std::sync::mpsc::SyncSender; fn rustfmt(src: &Path, rustfmt: &Path, paths: &[PathBuf], check: bool) -> impl FnMut(bool) -> bool { let mut cmd = Command::new(&rustfmt); // avoid the submodule config paths from coming into play, // we only allow a single global config for the workspace for now cmd.arg("--config-path").arg(&src.canonicalize().unwrap()); cmd.arg("--edition").arg("2021"); cmd.arg("--unstable-features"); cmd.arg("--skip-children"); if check { cmd.arg("--check"); } cmd.args(paths); let cmd_debug = format!("{cmd:?}"); let mut cmd = cmd.spawn().expect("running rustfmt"); // poor man's async: return a closure that'll wait for rustfmt's completion move |block: bool| -> bool { if !block { match cmd.try_wait() { Ok(Some(_)) => {} _ => return false, } } let status = cmd.wait().unwrap(); if !status.success() { eprintln!( "Running `{}` failed.\nIf you're running `tidy`, \ try again with `--bless`. Or, if you just want to format \ code, run `./x.py fmt` instead.", cmd_debug, ); crate::exit!(1); } true } } fn get_rustfmt_version(build: &Builder<'_>) -> Option<(String, PathBuf)> { let stamp_file = build.out.join("rustfmt.stamp"); let mut cmd = Command::new(match build.initial_rustfmt() { Some(p) => p, None => return None, }); cmd.arg("--version"); let output = match cmd.output() { Ok(status) => status, Err(_) => return None, }; if !output.status.success() { return None; } Some((String::from_utf8(output.stdout).unwrap(), stamp_file)) } /// Return whether the format cache can be reused. fn verify_rustfmt_version(build: &Builder<'_>) -> bool { let Some((version, stamp_file)) = get_rustfmt_version(build) else { return false; }; !program_out_of_date(&stamp_file, &version) } /// Updates the last rustfmt version used fn update_rustfmt_version(build: &Builder<'_>) { let Some((version, stamp_file)) = get_rustfmt_version(build) else { return; }; t!(std::fs::write(stamp_file, version)) } /// Returns the Rust files modified between the `merge-base` of HEAD and /// rust-lang/master and what is now on the disk. /// /// Returns `None` if all files should be formatted. fn get_modified_rs_files(build: &Builder<'_>) -> Result>, String> { if !verify_rustfmt_version(build) { return Ok(None); } get_git_modified_files(Some(&build.config.src), &vec!["rs"]) } #[derive(serde_derive::Deserialize)] struct RustfmtConfig { ignore: Vec, } pub fn format(build: &Builder<'_>, check: bool, paths: &[PathBuf]) { if build.config.dry_run() { return; } let mut builder = ignore::types::TypesBuilder::new(); builder.add_defaults(); builder.select("rust"); let matcher = builder.build().unwrap(); let rustfmt_config = build.src.join("rustfmt.toml"); if !rustfmt_config.exists() { eprintln!("Not running formatting checks; rustfmt.toml does not exist."); eprintln!("This may happen in distributed tarballs."); return; } let rustfmt_config = t!(std::fs::read_to_string(&rustfmt_config)); let rustfmt_config: RustfmtConfig = t!(toml::from_str(&rustfmt_config)); let mut fmt_override = ignore::overrides::OverrideBuilder::new(&build.src); for ignore in rustfmt_config.ignore { fmt_override.add(&format!("!{ignore}")).expect(&ignore); } let git_available = match Command::new("git") .arg("--version") .stdout(Stdio::null()) .stderr(Stdio::null()) .status() { Ok(status) => status.success(), Err(_) => false, }; if git_available { let in_working_tree = match build .config .git() .arg("rev-parse") .arg("--is-inside-work-tree") .stdout(Stdio::null()) .stderr(Stdio::null()) .status() { Ok(status) => status.success(), Err(_) => false, }; if in_working_tree { let untracked_paths_output = output( build.config.git().arg("status").arg("--porcelain").arg("--untracked-files=normal"), ); let untracked_paths = untracked_paths_output .lines() .filter(|entry| entry.starts_with("??")) .map(|entry| { entry.split(' ').nth(1).expect("every git status entry should list a path") }); let mut untracked_count = 0; for untracked_path in untracked_paths { println!("skip untracked path {untracked_path} during rustfmt invocations"); // The leading `/` makes it an exact match against the // repository root, rather than a glob. Without that, if you // have `foo.rs` in the repository root it will also match // against anything like `compiler/rustc_foo/src/foo.rs`, // preventing the latter from being formatted. untracked_count += 1; fmt_override.add(&format!("!/{untracked_path}")).expect(&untracked_path); } // Only check modified files locally to speed up runtime. // We still check all files in CI to avoid bugs in `get_modified_rs_files` letting regressions slip through; // we also care about CI time less since this is still very fast compared to building the compiler. if !CiEnv::is_ci() && paths.is_empty() { match get_modified_rs_files(build) { Ok(Some(files)) => { if files.len() <= 10 { for file in &files { println!("formatting modified file {file}"); } } else { let pluralized = |count| if count > 1 { "files" } else { "file" }; let untracked_msg = if untracked_count == 0 { "".to_string() } else { format!( ", skipped {} untracked {}", untracked_count, pluralized(untracked_count), ) }; println!( "formatting {} modified {}{}", files.len(), pluralized(files.len()), untracked_msg ); } for file in files { fmt_override.add(&format!("/{file}")).expect(&file); } } Ok(None) => {} Err(err) => { println!( "WARN: Something went wrong when running git commands:\n{err}\n\ Falling back to formatting all files." ); } } } } else { println!("Not in git tree. Skipping git-aware format checks"); } } else { println!("Could not find usable git. Skipping git-aware format checks"); } let fmt_override = fmt_override.build().unwrap(); let rustfmt_path = build.initial_rustfmt().unwrap_or_else(|| { eprintln!("./x.py fmt is not supported on this channel"); crate::exit!(1); }); assert!(rustfmt_path.exists(), "{}", rustfmt_path.display()); let src = build.src.clone(); let (tx, rx): (SyncSender, _) = std::sync::mpsc::sync_channel(128); let walker = match paths.get(0) { Some(first) => { let find_shortcut_candidates = |p: &PathBuf| { let mut candidates = Vec::new(); for candidate in WalkBuilder::new(src.clone()).max_depth(Some(3)).build() { if let Ok(entry) = candidate { if let Some(dir_name) = p.file_name() { if entry.path().is_dir() && entry.file_name() == dir_name { candidates.push(entry.into_path()); } } } } candidates }; // Only try to look for shortcut candidates for single component paths like // `std` and not for e.g. relative paths like `../library/std`. let should_look_for_shortcut_dir = |p: &PathBuf| p.components().count() == 1; let mut walker = if should_look_for_shortcut_dir(first) { if let [single_candidate] = &find_shortcut_candidates(first)[..] { WalkBuilder::new(single_candidate) } else { WalkBuilder::new(first) } } else { WalkBuilder::new(src.join(first)) }; for path in &paths[1..] { if should_look_for_shortcut_dir(path) { if let [single_candidate] = &find_shortcut_candidates(path)[..] { walker.add(single_candidate); } else { walker.add(path); } } else { walker.add(src.join(path)); } } walker } None => WalkBuilder::new(src.clone()), } .types(matcher) .overrides(fmt_override) .build_parallel(); // there is a lot of blocking involved in spawning a child process and reading files to format. // spawn more processes than available concurrency to keep the CPU busy let max_processes = build.jobs() as usize * 2; // spawn child processes on a separate thread so we can batch entries we have received from ignore let thread = std::thread::spawn(move || { let mut children = VecDeque::new(); while let Ok(path) = rx.recv() { // try getting a few more paths from the channel to amortize the overhead of spawning processes let paths: Vec<_> = rx.try_iter().take(7).chain(std::iter::once(path)).collect(); let child = rustfmt(&src, &rustfmt_path, paths.as_slice(), check); children.push_back(child); // poll completion before waiting for i in (0..children.len()).rev() { if children[i](false) { children.swap_remove_back(i); break; } } if children.len() >= max_processes { // await oldest child children.pop_front().unwrap()(true); } } // await remaining children for mut child in children { child(true); } }); walker.run(|| { let tx = tx.clone(); Box::new(move |entry| { let entry = t!(entry); if entry.file_type().map_or(false, |t| t.is_file()) { t!(tx.send(entry.into_path())); } ignore::WalkState::Continue }) }); drop(tx); thread.join().unwrap(); if !check { update_rustfmt_version(build); } }