]> git.lizzy.rs Git - rust.git/blob - src/bootstrap/format.rs
Rollup merge of #107544 - nnethercote:improve-TokenCursor, r=petrochenkov
[rust.git] / src / bootstrap / format.rs
1 //! Runs rustfmt on the repository.
2
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;
11
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");
20     if check {
21         cmd.arg("--check");
22     }
23     cmd.args(paths);
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 {
28         if !block {
29             match cmd.try_wait() {
30                 Ok(Some(_)) => {}
31                 _ => return false,
32             }
33         }
34         let status = cmd.wait().unwrap();
35         if !status.success() {
36             eprintln!(
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.",
40                 cmd_debug,
41             );
42             crate::detail_exit(1);
43         }
44         true
45     }
46 }
47
48 fn get_rustfmt_version(build: &Builder<'_>) -> Option<(String, PathBuf)> {
49     let stamp_file = build.out.join("rustfmt.stamp");
50
51     let mut cmd = Command::new(match build.initial_rustfmt() {
52         Some(p) => p,
53         None => return None,
54     });
55     cmd.arg("--version");
56     let output = match cmd.output() {
57         Ok(status) => status,
58         Err(_) => return None,
59     };
60     if !output.status.success() {
61         return None;
62     }
63     Some((String::from_utf8(output.stdout).unwrap(), stamp_file))
64 }
65
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)
70 }
71
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))
76 }
77
78 /// Returns the Rust files modified between the `merge-base` of HEAD and
79 /// rust-lang/master and what is now on the disk.
80 ///
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); };
84
85     if !verify_rustfmt_version(build) {
86         return Ok(None);
87     }
88
89     let merge_base =
90         output_result(build.config.git().arg("merge-base").arg(&updated_master).arg("HEAD"))?;
91     Ok(Some(
92         output_result(
93             build.config.git().arg("diff-index").arg("--name-only").arg(merge_base.trim()),
94         )?
95         .lines()
96         .map(|s| s.trim().to_owned())
97         .filter(|f| Path::new(f).extension().map_or(false, |ext| ext == "rs"))
98         .collect(),
99     ))
100 }
101
102 #[derive(serde::Deserialize)]
103 struct RustfmtConfig {
104     ignore: Vec<String>,
105 }
106
107 pub fn format(build: &Builder<'_>, check: bool, paths: &[PathBuf]) {
108     if build.config.dry_run() {
109         return;
110     }
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.");
119         return;
120     }
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);
126     }
127     let git_available = match Command::new("git")
128         .arg("--version")
129         .stdout(Stdio::null())
130         .stderr(Stdio::null())
131         .status()
132     {
133         Ok(status) => status.success(),
134         Err(_) => false,
135     };
136
137     let mut paths = paths.to_vec();
138
139     if git_available {
140         let in_working_tree = match build
141             .config
142             .git()
143             .arg("rev-parse")
144             .arg("--is-inside-work-tree")
145             .stdout(Stdio::null())
146             .stderr(Stdio::null())
147             .status()
148         {
149             Ok(status) => status.success(),
150             Err(_) => false,
151         };
152         if in_working_tree {
153             let untracked_paths_output = output(
154                 build.config.git().arg("status").arg("--porcelain").arg("--untracked-files=normal"),
155             );
156             let untracked_paths = untracked_paths_output
157                 .lines()
158                 .filter(|entry| entry.starts_with("??"))
159                 .map(|entry| {
160                     entry.split(' ').nth(1).expect("every git status entry should list a path")
161                 });
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);
170             }
171             if !check && paths.is_empty() {
172                 match get_modified_rs_files(build) {
173                     Ok(Some(files)) => {
174                         for file in files {
175                             println!("formatting modified file {file}");
176                             ignore_fmt.add(&format!("/{file}")).expect(&file);
177                         }
178                     }
179                     Ok(None) => {}
180                     Err(err) => {
181                         println!(
182                             "WARN: Something went wrong when running git commands:\n{err}\n\
183                             Falling back to formatting all files."
184                         );
185                         // Something went wrong when getting the version. Just format all the files.
186                         paths.push(".".into());
187                     }
188                 }
189             }
190         } else {
191             println!("Not in git tree. Skipping git-aware format checks");
192         }
193     } else {
194         println!("Could not find usable git. Skipping git-aware format checks");
195     }
196
197     let ignore_fmt = ignore_fmt.build().unwrap();
198
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);
202     });
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) {
207         Some(first) => {
208             let mut walker = WalkBuilder::new(first);
209             for path in &paths[1..] {
210                 walker.add(path);
211             }
212             walker
213         }
214         None => WalkBuilder::new(src.clone()),
215     }
216     .types(matcher)
217     .overrides(ignore_fmt)
218     .build_parallel();
219
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;
223
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();
230
231             let child = rustfmt(&src, &rustfmt_path, paths.as_slice(), check);
232             children.push_back(child);
233
234             // poll completion before waiting
235             for i in (0..children.len()).rev() {
236                 if children[i](false) {
237                     children.swap_remove_back(i);
238                     break;
239                 }
240             }
241
242             if children.len() >= max_processes {
243                 // await oldest child
244                 children.pop_front().unwrap()(true);
245             }
246         }
247
248         // await remaining children
249         for mut child in children {
250             child(true);
251         }
252     });
253
254     walker.run(|| {
255         let tx = tx.clone();
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()));
260             }
261             ignore::WalkState::Continue
262         })
263     });
264
265     drop(tx);
266
267     thread.join().unwrap();
268     if !check {
269         update_rustfmt_version(build);
270     }
271 }