1 //! Runs rustfmt on the repository.
3 use crate::builder::Builder;
4 use crate::util::{output, output_result, program_out_of_date, t};
5 use build_helper::git::updated_master_branch;
6 use ignore::WalkBuilder;
7 use std::collections::VecDeque;
8 use std::path::{Path, PathBuf};
9 use std::process::{Command, Stdio};
10 use std::sync::mpsc::SyncSender;
12 fn rustfmt(src: &Path, rustfmt: &Path, paths: &[PathBuf], check: bool) -> impl FnMut(bool) -> bool {
13 let mut cmd = Command::new(&rustfmt);
14 // avoid the submodule config paths from coming into play,
15 // we only allow a single global config for the workspace for now
16 cmd.arg("--config-path").arg(&src.canonicalize().unwrap());
17 cmd.arg("--edition").arg("2021");
18 cmd.arg("--unstable-features");
19 cmd.arg("--skip-children");
24 let cmd_debug = format!("{:?}", cmd);
25 let mut cmd = cmd.spawn().expect("running rustfmt");
26 // poor man's async: return a closure that'll wait for rustfmt's completion
27 move |block: bool| -> bool {
29 match cmd.try_wait() {
34 let status = cmd.wait().unwrap();
35 if !status.success() {
37 "Running `{}` failed.\nIf you're running `tidy`, \
38 try again with `--bless`. Or, if you just want to format \
39 code, run `./x.py fmt` instead.",
42 crate::detail_exit(1);
48 fn get_rustfmt_version(build: &Builder<'_>) -> Option<(String, PathBuf)> {
49 let stamp_file = build.out.join("rustfmt.stamp");
51 let mut cmd = Command::new(match build.initial_rustfmt() {
56 let output = match cmd.output() {
58 Err(_) => return None,
60 if !output.status.success() {
63 Some((String::from_utf8(output.stdout).unwrap(), stamp_file))
66 /// Return whether the format cache can be reused.
67 fn verify_rustfmt_version(build: &Builder<'_>) -> bool {
68 let Some((version, stamp_file)) = get_rustfmt_version(build) else {return false;};
69 !program_out_of_date(&stamp_file, &version)
72 /// Updates the last rustfmt version used
73 fn update_rustfmt_version(build: &Builder<'_>) {
74 let Some((version, stamp_file)) = get_rustfmt_version(build) else {return;};
75 t!(std::fs::write(stamp_file, version))
78 /// Returns the Rust files modified between the `merge-base` of HEAD and
79 /// rust-lang/master and what is now on the disk.
81 /// Returns `None` if all files should be formatted.
82 fn get_modified_rs_files(build: &Builder<'_>) -> Result<Option<Vec<String>>, String> {
83 let Ok(updated_master) = updated_master_branch(Some(&build.config.src)) else { return Ok(None); };
85 if !verify_rustfmt_version(build) {
90 output_result(build.config.git().arg("merge-base").arg(&updated_master).arg("HEAD"))?;
93 build.config.git().arg("diff-index").arg("--name-only").arg(merge_base.trim()),
96 .map(|s| s.trim().to_owned())
97 .filter(|f| Path::new(f).extension().map_or(false, |ext| ext == "rs"))
102 #[derive(serde::Deserialize)]
103 struct RustfmtConfig {
107 pub fn format(build: &Builder<'_>, check: bool, paths: &[PathBuf]) {
108 if build.config.dry_run() {
111 let mut builder = ignore::types::TypesBuilder::new();
112 builder.add_defaults();
113 builder.select("rust");
114 let matcher = builder.build().unwrap();
115 let rustfmt_config = build.src.join("rustfmt.toml");
116 if !rustfmt_config.exists() {
117 eprintln!("Not running formatting checks; rustfmt.toml does not exist.");
118 eprintln!("This may happen in distributed tarballs.");
121 let rustfmt_config = t!(std::fs::read_to_string(&rustfmt_config));
122 let rustfmt_config: RustfmtConfig = t!(toml::from_str(&rustfmt_config));
123 let mut ignore_fmt = ignore::overrides::OverrideBuilder::new(&build.src);
124 for ignore in rustfmt_config.ignore {
125 ignore_fmt.add(&format!("!{}", ignore)).expect(&ignore);
127 let git_available = match Command::new("git")
129 .stdout(Stdio::null())
130 .stderr(Stdio::null())
133 Ok(status) => status.success(),
137 let mut paths = paths.to_vec();
140 let in_working_tree = match build
144 .arg("--is-inside-work-tree")
145 .stdout(Stdio::null())
146 .stderr(Stdio::null())
149 Ok(status) => status.success(),
153 let untracked_paths_output = output(
154 build.config.git().arg("status").arg("--porcelain").arg("--untracked-files=normal"),
156 let untracked_paths = untracked_paths_output
158 .filter(|entry| entry.starts_with("??"))
160 entry.split(' ').nth(1).expect("every git status entry should list a path")
162 for untracked_path in untracked_paths {
163 println!("skip untracked path {} during rustfmt invocations", untracked_path);
164 // The leading `/` makes it an exact match against the
165 // repository root, rather than a glob. Without that, if you
166 // have `foo.rs` in the repository root it will also match
167 // against anything like `compiler/rustc_foo/src/foo.rs`,
168 // preventing the latter from being formatted.
169 ignore_fmt.add(&format!("!/{}", untracked_path)).expect(&untracked_path);
171 if !check && paths.is_empty() {
172 match get_modified_rs_files(build) {
175 println!("formatting modified file {file}");
176 ignore_fmt.add(&format!("/{file}")).expect(&file);
182 "WARN: Something went wrong when running git commands:\n{err}\n\
183 Falling back to formatting all files."
185 // Something went wrong when getting the version. Just format all the files.
186 paths.push(".".into());
191 println!("Not in git tree. Skipping git-aware format checks");
194 println!("Could not find usable git. Skipping git-aware format checks");
197 let ignore_fmt = ignore_fmt.build().unwrap();
199 let rustfmt_path = build.initial_rustfmt().unwrap_or_else(|| {
200 eprintln!("./x.py fmt is not supported on this channel");
201 crate::detail_exit(1);
203 assert!(rustfmt_path.exists(), "{}", rustfmt_path.display());
204 let src = build.src.clone();
205 let (tx, rx): (SyncSender<PathBuf>, _) = std::sync::mpsc::sync_channel(128);
206 let walker = match paths.get(0) {
208 let mut walker = WalkBuilder::new(first);
209 for path in &paths[1..] {
214 None => WalkBuilder::new(src.clone()),
217 .overrides(ignore_fmt)
220 // there is a lot of blocking involved in spawning a child process and reading files to format.
221 // spawn more processes than available concurrency to keep the CPU busy
222 let max_processes = build.jobs() as usize * 2;
224 // spawn child processes on a separate thread so we can batch entries we have received from ignore
225 let thread = std::thread::spawn(move || {
226 let mut children = VecDeque::new();
227 while let Ok(path) = rx.recv() {
228 // try getting a few more paths from the channel to amortize the overhead of spawning processes
229 let paths: Vec<_> = rx.try_iter().take(7).chain(std::iter::once(path)).collect();
231 let child = rustfmt(&src, &rustfmt_path, paths.as_slice(), check);
232 children.push_back(child);
234 // poll completion before waiting
235 for i in (0..children.len()).rev() {
236 if children[i](false) {
237 children.swap_remove_back(i);
242 if children.len() >= max_processes {
243 // await oldest child
244 children.pop_front().unwrap()(true);
248 // await remaining children
249 for mut child in children {
256 Box::new(move |entry| {
257 let entry = t!(entry);
258 if entry.file_type().map_or(false, |t| t.is_file()) {
259 t!(tx.send(entry.into_path()));
261 ignore::WalkState::Continue
267 thread.join().unwrap();
269 update_rustfmt_version(build);