1 //! Runs rustfmt on the repository.
3 use crate::builder::Builder;
4 use crate::util::{output, program_out_of_date, t};
5 use ignore::WalkBuilder;
6 use std::collections::VecDeque;
7 use std::path::{Path, PathBuf};
8 use std::process::{Command, Stdio};
9 use std::sync::mpsc::SyncSender;
11 fn rustfmt(src: &Path, rustfmt: &Path, paths: &[PathBuf], check: bool) -> impl FnMut(bool) -> bool {
12 let mut cmd = Command::new(&rustfmt);
13 // avoid the submodule config paths from coming into play,
14 // we only allow a single global config for the workspace for now
15 cmd.arg("--config-path").arg(&src.canonicalize().unwrap());
16 cmd.arg("--edition").arg("2021");
17 cmd.arg("--unstable-features");
18 cmd.arg("--skip-children");
23 let cmd_debug = format!("{:?}", cmd);
24 let mut cmd = cmd.spawn().expect("running rustfmt");
25 // poor man's async: return a closure that'll wait for rustfmt's completion
26 move |block: bool| -> bool {
28 match cmd.try_wait() {
33 let status = cmd.wait().unwrap();
34 if !status.success() {
36 "Running `{}` failed.\nIf you're running `tidy`, \
37 try again with `--bless`. Or, if you just want to format \
38 code, run `./x.py fmt` instead.",
41 crate::detail_exit(1);
47 fn get_rustfmt_version(build: &Builder<'_>) -> Option<(String, PathBuf)> {
48 let stamp_file = build.out.join("rustfmt.stamp");
50 let mut cmd = Command::new(match build.initial_rustfmt() {
55 let output = match cmd.output() {
57 Err(_) => return None,
59 if !output.status.success() {
62 Some((String::from_utf8(output.stdout).unwrap(), stamp_file))
65 /// Return whether the format cache can be reused.
66 fn verify_rustfmt_version(build: &Builder<'_>) -> bool {
67 let Some((version, stamp_file)) = get_rustfmt_version(build) else {return false;};
68 !program_out_of_date(&stamp_file, &version)
71 /// Updates the last rustfmt version used
72 fn update_rustfmt_version(build: &Builder<'_>) {
73 let Some((version, stamp_file)) = get_rustfmt_version(build) else {return;};
74 t!(std::fs::write(stamp_file, version))
77 /// Returns the Rust files modified between the `merge-base` of HEAD and
78 /// rust-lang/master and what is now on the disk.
80 /// Returns `None` if all files should be formatted.
81 fn get_modified_rs_files(build: &Builder<'_>) -> Option<Vec<String>> {
82 let Ok(remote) = get_rust_lang_rust_remote() else { return None; };
83 if !verify_rustfmt_version(build) {
88 output(build.config.git().arg("merge-base").arg(&format!("{remote}/master")).arg("HEAD"));
90 output(build.config.git().arg("diff-index").arg("--name-only").arg(merge_base.trim()))
92 .map(|s| s.trim().to_owned())
93 .filter(|f| Path::new(f).extension().map_or(false, |ext| ext == "rs"))
98 /// Finds the remote for rust-lang/rust.
99 /// For example for these remotes it will return `upstream`.
101 /// origin https://github.com/Nilstrieb/rust.git (fetch)
102 /// origin https://github.com/Nilstrieb/rust.git (push)
103 /// upstream https://github.com/rust-lang/rust (fetch)
104 /// upstream https://github.com/rust-lang/rust (push)
106 fn get_rust_lang_rust_remote() -> Result<String, String> {
107 let mut git = Command::new("git");
108 git.args(["config", "--local", "--get-regex", "remote\\..*\\.url"]);
110 let output = git.output().map_err(|err| format!("{err:?}"))?;
111 if !output.status.success() {
112 return Err("failed to execute git config command".to_owned());
115 let stdout = String::from_utf8(output.stdout).map_err(|err| format!("{err:?}"))?;
117 let rust_lang_remote = stdout
119 .find(|remote| remote.contains("rust-lang"))
120 .ok_or_else(|| "rust-lang/rust remote not found".to_owned())?;
123 rust_lang_remote.split('.').nth(1).ok_or_else(|| "remote name not found".to_owned())?;
124 Ok(remote_name.into())
127 #[derive(serde::Deserialize)]
128 struct RustfmtConfig {
132 pub fn format(build: &Builder<'_>, check: bool, paths: &[PathBuf]) {
133 if build.config.dry_run() {
136 let mut builder = ignore::types::TypesBuilder::new();
137 builder.add_defaults();
138 builder.select("rust");
139 let matcher = builder.build().unwrap();
140 let rustfmt_config = build.src.join("rustfmt.toml");
141 if !rustfmt_config.exists() {
142 eprintln!("Not running formatting checks; rustfmt.toml does not exist.");
143 eprintln!("This may happen in distributed tarballs.");
146 let rustfmt_config = t!(std::fs::read_to_string(&rustfmt_config));
147 let rustfmt_config: RustfmtConfig = t!(toml::from_str(&rustfmt_config));
148 let mut ignore_fmt = ignore::overrides::OverrideBuilder::new(&build.src);
149 for ignore in rustfmt_config.ignore {
150 ignore_fmt.add(&format!("!{}", ignore)).expect(&ignore);
152 let git_available = match Command::new("git")
154 .stdout(Stdio::null())
155 .stderr(Stdio::null())
158 Ok(status) => status.success(),
162 let in_working_tree = match build
166 .arg("--is-inside-work-tree")
167 .stdout(Stdio::null())
168 .stderr(Stdio::null())
171 Ok(status) => status.success(),
175 let untracked_paths_output = output(
176 build.config.git().arg("status").arg("--porcelain").arg("--untracked-files=normal"),
178 let untracked_paths = untracked_paths_output
180 .filter(|entry| entry.starts_with("??"))
182 entry.split(' ').nth(1).expect("every git status entry should list a path")
184 for untracked_path in untracked_paths {
185 println!("skip untracked path {} during rustfmt invocations", untracked_path);
186 // The leading `/` makes it an exact match against the
187 // repository root, rather than a glob. Without that, if you
188 // have `foo.rs` in the repository root it will also match
189 // against anything like `compiler/rustc_foo/src/foo.rs`,
190 // preventing the latter from being formatted.
191 ignore_fmt.add(&format!("!/{}", untracked_path)).expect(&untracked_path);
193 if !check && paths.is_empty() {
194 if let Some(files) = get_modified_rs_files(build) {
196 println!("formatting modified file {file}");
197 ignore_fmt.add(&format!("/{file}")).expect(&file);
202 println!("Not in git tree. Skipping git-aware format checks");
205 println!("Could not find usable git. Skipping git-aware format checks");
207 let ignore_fmt = ignore_fmt.build().unwrap();
209 let rustfmt_path = build.initial_rustfmt().unwrap_or_else(|| {
210 eprintln!("./x.py fmt is not supported on this channel");
211 crate::detail_exit(1);
213 assert!(rustfmt_path.exists(), "{}", rustfmt_path.display());
214 let src = build.src.clone();
215 let (tx, rx): (SyncSender<PathBuf>, _) = std::sync::mpsc::sync_channel(128);
216 let walker = match paths.get(0) {
218 let mut walker = WalkBuilder::new(first);
219 for path in &paths[1..] {
224 None => WalkBuilder::new(src.clone()),
227 .overrides(ignore_fmt)
230 // there is a lot of blocking involved in spawning a child process and reading files to format.
231 // spawn more processes than available concurrency to keep the CPU busy
232 let max_processes = build.jobs() as usize * 2;
234 // spawn child processes on a separate thread so we can batch entries we have received from ignore
235 let thread = std::thread::spawn(move || {
236 let mut children = VecDeque::new();
237 while let Ok(path) = rx.recv() {
238 // try getting a few more paths from the channel to amortize the overhead of spawning processes
239 let paths: Vec<_> = rx.try_iter().take(7).chain(std::iter::once(path)).collect();
241 let child = rustfmt(&src, &rustfmt_path, paths.as_slice(), check);
242 children.push_back(child);
244 // poll completion before waiting
245 for i in (0..children.len()).rev() {
246 if children[i](false) {
247 children.swap_remove_back(i);
252 if children.len() >= max_processes {
253 // await oldest child
254 children.pop_front().unwrap()(true);
258 // await remaining children
259 for mut child in children {
266 Box::new(move |entry| {
267 let entry = t!(entry);
268 if entry.file_type().map_or(false, |t| t.is_file()) {
269 t!(tx.send(entry.into_path()));
271 ignore::WalkState::Continue
277 thread.join().unwrap();
279 update_rustfmt_version(build);