]> git.lizzy.rs Git - rust.git/blob - src/bootstrap/format.rs
Rollup merge of #105863 - GuillaumeGomez:update-browser-ui-test, r=notriddle
[rust.git] / src / bootstrap / format.rs
1 //! Runs rustfmt on the repository.
2
3 use crate::builder::Builder;
4 use crate::util::{output, 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 #[derive(serde::Deserialize)]
48 struct RustfmtConfig {
49     ignore: Vec<String>,
50 }
51
52 pub fn format(build: &Builder<'_>, check: bool, paths: &[PathBuf]) {
53     if build.config.dry_run() {
54         return;
55     }
56     let mut builder = ignore::types::TypesBuilder::new();
57     builder.add_defaults();
58     builder.select("rust");
59     let matcher = builder.build().unwrap();
60     let rustfmt_config = build.src.join("rustfmt.toml");
61     if !rustfmt_config.exists() {
62         eprintln!("Not running formatting checks; rustfmt.toml does not exist.");
63         eprintln!("This may happen in distributed tarballs.");
64         return;
65     }
66     let rustfmt_config = t!(std::fs::read_to_string(&rustfmt_config));
67     let rustfmt_config: RustfmtConfig = t!(toml::from_str(&rustfmt_config));
68     let mut ignore_fmt = ignore::overrides::OverrideBuilder::new(&build.src);
69     for ignore in rustfmt_config.ignore {
70         ignore_fmt.add(&format!("!{}", ignore)).expect(&ignore);
71     }
72     let git_available = match Command::new("git")
73         .arg("--version")
74         .stdout(Stdio::null())
75         .stderr(Stdio::null())
76         .status()
77     {
78         Ok(status) => status.success(),
79         Err(_) => false,
80     };
81     if git_available {
82         let in_working_tree = match build
83             .config
84             .git()
85             .arg("rev-parse")
86             .arg("--is-inside-work-tree")
87             .stdout(Stdio::null())
88             .stderr(Stdio::null())
89             .status()
90         {
91             Ok(status) => status.success(),
92             Err(_) => false,
93         };
94         if in_working_tree {
95             let untracked_paths_output = output(
96                 build.config.git().arg("status").arg("--porcelain").arg("--untracked-files=normal"),
97             );
98             let untracked_paths = untracked_paths_output
99                 .lines()
100                 .filter(|entry| entry.starts_with("??"))
101                 .map(|entry| {
102                     entry.split(' ').nth(1).expect("every git status entry should list a path")
103                 });
104             for untracked_path in untracked_paths {
105                 println!("skip untracked path {} during rustfmt invocations", untracked_path);
106                 // The leading `/` makes it an exact match against the
107                 // repository root, rather than a glob. Without that, if you
108                 // have `foo.rs` in the repository root it will also match
109                 // against anything like `compiler/rustc_foo/src/foo.rs`,
110                 // preventing the latter from being formatted.
111                 ignore_fmt.add(&format!("!/{}", untracked_path)).expect(&untracked_path);
112             }
113         } else {
114             println!("Not in git tree. Skipping git-aware format checks");
115         }
116     } else {
117         println!("Could not find usable git. Skipping git-aware format checks");
118     }
119     let ignore_fmt = ignore_fmt.build().unwrap();
120
121     let rustfmt_path = build.initial_rustfmt().unwrap_or_else(|| {
122         eprintln!("./x.py fmt is not supported on this channel");
123         crate::detail_exit(1);
124     });
125     assert!(rustfmt_path.exists(), "{}", rustfmt_path.display());
126     let src = build.src.clone();
127     let (tx, rx): (SyncSender<PathBuf>, _) = std::sync::mpsc::sync_channel(128);
128     let walker = match paths.get(0) {
129         Some(first) => {
130             let mut walker = WalkBuilder::new(first);
131             for path in &paths[1..] {
132                 walker.add(path);
133             }
134             walker
135         }
136         None => WalkBuilder::new(src.clone()),
137     }
138     .types(matcher)
139     .overrides(ignore_fmt)
140     .build_parallel();
141
142     // there is a lot of blocking involved in spawning a child process and reading files to format.
143     // spawn more processes than available concurrency to keep the CPU busy
144     let max_processes = build.jobs() as usize * 2;
145
146     // spawn child processes on a separate thread so we can batch entries we have received from ignore
147     let thread = std::thread::spawn(move || {
148         let mut children = VecDeque::new();
149         while let Ok(path) = rx.recv() {
150             // try getting a few more paths from the channel to amortize the overhead of spawning processes
151             let paths: Vec<_> = rx.try_iter().take(7).chain(std::iter::once(path)).collect();
152
153             let child = rustfmt(&src, &rustfmt_path, paths.as_slice(), check);
154             children.push_back(child);
155
156             // poll completion before waiting
157             for i in (0..children.len()).rev() {
158                 if children[i](false) {
159                     children.swap_remove_back(i);
160                     break;
161                 }
162             }
163
164             if children.len() >= max_processes {
165                 // await oldest child
166                 children.pop_front().unwrap()(true);
167             }
168         }
169
170         // await remaining children
171         for mut child in children {
172             child(true);
173         }
174     });
175
176     walker.run(|| {
177         let tx = tx.clone();
178         Box::new(move |entry| {
179             let entry = t!(entry);
180             if entry.file_type().map_or(false, |t| t.is_file()) {
181                 t!(tx.send(entry.into_path()));
182             }
183             ignore::WalkState::Continue
184         })
185     });
186
187     drop(tx);
188
189     thread.join().unwrap();
190 }