]> git.lizzy.rs Git - rust.git/blob - src/bootstrap/format.rs
Auto merge of #105651 - tgross35:once-cell-inline, r=m-ou-se
[rust.git] / src / bootstrap / format.rs
1 //! Runs rustfmt on the repository.
2
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;
10
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");
19     if check {
20         cmd.arg("--check");
21     }
22     cmd.args(paths);
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 {
27         if !block {
28             match cmd.try_wait() {
29                 Ok(Some(_)) => {}
30                 _ => return false,
31             }
32         }
33         let status = cmd.wait().unwrap();
34         if !status.success() {
35             eprintln!(
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.",
39                 cmd_debug,
40             );
41             crate::detail_exit(1);
42         }
43         true
44     }
45 }
46
47 fn get_rustfmt_version(build: &Builder<'_>) -> Option<(String, PathBuf)> {
48     let stamp_file = build.out.join("rustfmt.stamp");
49
50     let mut cmd = Command::new(match build.initial_rustfmt() {
51         Some(p) => p,
52         None => return None,
53     });
54     cmd.arg("--version");
55     let output = match cmd.output() {
56         Ok(status) => status,
57         Err(_) => return None,
58     };
59     if !output.status.success() {
60         return None;
61     }
62     Some((String::from_utf8(output.stdout).unwrap(), stamp_file))
63 }
64
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)
69 }
70
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))
75 }
76
77 /// Returns the Rust files modified between the `merge-base` of HEAD and
78 /// rust-lang/master and what is now on the disk.
79 ///
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) {
84         return None;
85     }
86     Some(
87         output(
88             build
89                 .config
90                 .git()
91                 .arg("diff-index")
92                 .arg("--name-only")
93                 .arg("--merge-base")
94                 .arg(&format!("{remote}/master")),
95         )
96         .lines()
97         .map(|s| s.trim().to_owned())
98         .filter(|f| Path::new(f).extension().map_or(false, |ext| ext == "rs"))
99         .collect(),
100     )
101 }
102
103 /// Finds the remote for rust-lang/rust.
104 /// For example for these remotes it will return `upstream`.
105 /// ```text
106 /// origin  https://github.com/Nilstrieb/rust.git (fetch)
107 /// origin  https://github.com/Nilstrieb/rust.git (push)
108 /// upstream        https://github.com/rust-lang/rust (fetch)
109 /// upstream        https://github.com/rust-lang/rust (push)
110 /// ```
111 fn get_rust_lang_rust_remote() -> Result<String, String> {
112     let mut git = Command::new("git");
113     git.args(["config", "--local", "--get-regex", "remote\\..*\\.url"]);
114
115     let output = git.output().map_err(|err| format!("{err:?}"))?;
116     if !output.status.success() {
117         return Err("failed to execute git config command".to_owned());
118     }
119
120     let stdout = String::from_utf8(output.stdout).map_err(|err| format!("{err:?}"))?;
121
122     let rust_lang_remote = stdout
123         .lines()
124         .find(|remote| remote.contains("rust-lang"))
125         .ok_or_else(|| "rust-lang/rust remote not found".to_owned())?;
126
127     let remote_name =
128         rust_lang_remote.split('.').nth(1).ok_or_else(|| "remote name not found".to_owned())?;
129     Ok(remote_name.into())
130 }
131
132 #[derive(serde::Deserialize)]
133 struct RustfmtConfig {
134     ignore: Vec<String>,
135 }
136
137 pub fn format(build: &Builder<'_>, check: bool, paths: &[PathBuf]) {
138     if build.config.dry_run() {
139         return;
140     }
141     let mut builder = ignore::types::TypesBuilder::new();
142     builder.add_defaults();
143     builder.select("rust");
144     let matcher = builder.build().unwrap();
145     let rustfmt_config = build.src.join("rustfmt.toml");
146     if !rustfmt_config.exists() {
147         eprintln!("Not running formatting checks; rustfmt.toml does not exist.");
148         eprintln!("This may happen in distributed tarballs.");
149         return;
150     }
151     let rustfmt_config = t!(std::fs::read_to_string(&rustfmt_config));
152     let rustfmt_config: RustfmtConfig = t!(toml::from_str(&rustfmt_config));
153     let mut ignore_fmt = ignore::overrides::OverrideBuilder::new(&build.src);
154     for ignore in rustfmt_config.ignore {
155         ignore_fmt.add(&format!("!{}", ignore)).expect(&ignore);
156     }
157     let git_available = match Command::new("git")
158         .arg("--version")
159         .stdout(Stdio::null())
160         .stderr(Stdio::null())
161         .status()
162     {
163         Ok(status) => status.success(),
164         Err(_) => false,
165     };
166     if git_available {
167         let in_working_tree = match build
168             .config
169             .git()
170             .arg("rev-parse")
171             .arg("--is-inside-work-tree")
172             .stdout(Stdio::null())
173             .stderr(Stdio::null())
174             .status()
175         {
176             Ok(status) => status.success(),
177             Err(_) => false,
178         };
179         if in_working_tree {
180             let untracked_paths_output = output(
181                 build.config.git().arg("status").arg("--porcelain").arg("--untracked-files=normal"),
182             );
183             let untracked_paths = untracked_paths_output
184                 .lines()
185                 .filter(|entry| entry.starts_with("??"))
186                 .map(|entry| {
187                     entry.split(' ').nth(1).expect("every git status entry should list a path")
188                 });
189             for untracked_path in untracked_paths {
190                 println!("skip untracked path {} during rustfmt invocations", untracked_path);
191                 // The leading `/` makes it an exact match against the
192                 // repository root, rather than a glob. Without that, if you
193                 // have `foo.rs` in the repository root it will also match
194                 // against anything like `compiler/rustc_foo/src/foo.rs`,
195                 // preventing the latter from being formatted.
196                 ignore_fmt.add(&format!("!/{}", untracked_path)).expect(&untracked_path);
197             }
198             if !check && paths.is_empty() {
199                 if let Some(files) = get_modified_rs_files(build) {
200                     for file in files {
201                         println!("formatting modified file {file}");
202                         ignore_fmt.add(&format!("/{file}")).expect(&file);
203                     }
204                 }
205             }
206         } else {
207             println!("Not in git tree. Skipping git-aware format checks");
208         }
209     } else {
210         println!("Could not find usable git. Skipping git-aware format checks");
211     }
212     let ignore_fmt = ignore_fmt.build().unwrap();
213
214     let rustfmt_path = build.initial_rustfmt().unwrap_or_else(|| {
215         eprintln!("./x.py fmt is not supported on this channel");
216         crate::detail_exit(1);
217     });
218     assert!(rustfmt_path.exists(), "{}", rustfmt_path.display());
219     let src = build.src.clone();
220     let (tx, rx): (SyncSender<PathBuf>, _) = std::sync::mpsc::sync_channel(128);
221     let walker = match paths.get(0) {
222         Some(first) => {
223             let mut walker = WalkBuilder::new(first);
224             for path in &paths[1..] {
225                 walker.add(path);
226             }
227             walker
228         }
229         None => WalkBuilder::new(src.clone()),
230     }
231     .types(matcher)
232     .overrides(ignore_fmt)
233     .build_parallel();
234
235     // there is a lot of blocking involved in spawning a child process and reading files to format.
236     // spawn more processes than available concurrency to keep the CPU busy
237     let max_processes = build.jobs() as usize * 2;
238
239     // spawn child processes on a separate thread so we can batch entries we have received from ignore
240     let thread = std::thread::spawn(move || {
241         let mut children = VecDeque::new();
242         while let Ok(path) = rx.recv() {
243             // try getting a few more paths from the channel to amortize the overhead of spawning processes
244             let paths: Vec<_> = rx.try_iter().take(7).chain(std::iter::once(path)).collect();
245
246             let child = rustfmt(&src, &rustfmt_path, paths.as_slice(), check);
247             children.push_back(child);
248
249             // poll completion before waiting
250             for i in (0..children.len()).rev() {
251                 if children[i](false) {
252                     children.swap_remove_back(i);
253                     break;
254                 }
255             }
256
257             if children.len() >= max_processes {
258                 // await oldest child
259                 children.pop_front().unwrap()(true);
260             }
261         }
262
263         // await remaining children
264         for mut child in children {
265             child(true);
266         }
267     });
268
269     walker.run(|| {
270         let tx = tx.clone();
271         Box::new(move |entry| {
272             let entry = t!(entry);
273             if entry.file_type().map_or(false, |t| t.is_file()) {
274                 t!(tx.send(entry.into_path()));
275             }
276             ignore::WalkState::Continue
277         })
278     });
279
280     drop(tx);
281
282     thread.join().unwrap();
283     if !check {
284         update_rustfmt_version(build);
285     }
286 }