1use std::collections::HashSet;
2use std::fmt::{Display, Formatter};
3use std::io;
4use std::path::{Path, PathBuf};
5use std::sync::{Arc, Mutex};
6
7use build_helper::ci::CiEnv;
8use build_helper::git::{GitConfig, get_closest_upstream_commit};
9use build_helper::stage0_parser::{Stage0Config, parse_stage0_file};
10use termcolor::Color;
11
12#[derive(Clone, Default)]
14pub struct TidyFlags {
15 bless: bool,
17}
18
19impl TidyFlags {
20 pub fn new(bless: bool) -> Self {
21 Self { bless }
22 }
23}
24
25#[derive(Clone)]
31pub struct TidyCtx {
32 tidy_flags: TidyFlags,
33 diag_ctx: Arc<Mutex<DiagCtxInner>>,
34 ci_env: CiEnv,
35 pub base_commit: Option<String>,
36}
37
38impl TidyCtx {
39 pub fn new(
40 root_path: &Path,
41 verbose: bool,
42 ci_flag: Option<bool>,
43 tidy_flags: TidyFlags,
44 ) -> Self {
45 let ci_env = match ci_flag {
46 Some(true) => CiEnv::GitHubActions,
47 Some(false) => CiEnv::None,
48 None => CiEnv::current(),
49 };
50
51 let mut tidy_ctx = Self {
52 diag_ctx: Arc::new(Mutex::new(DiagCtxInner {
53 running_checks: Default::default(),
54 finished_checks: Default::default(),
55 root_path: root_path.to_path_buf(),
56 verbose,
57 })),
58 tidy_flags,
59 ci_env,
60 base_commit: None,
61 };
62 tidy_ctx.base_commit = find_base_commit(&tidy_ctx);
63
64 tidy_ctx
65 }
66
67 pub fn is_bless_enabled(&self) -> bool {
68 self.tidy_flags.bless
69 }
70
71 pub fn is_running_on_ci(&self) -> bool {
72 self.ci_env.is_running_in_ci()
73 }
74
75 pub fn start_check<Id: Into<CheckId>>(&self, id: Id) -> RunningCheck {
76 let mut id = id.into();
77
78 let mut ctx = self.diag_ctx.lock().unwrap();
79
80 id.path = match id.path {
82 Some(path) => Some(path.strip_prefix(&ctx.root_path).unwrap_or(&path).to_path_buf()),
83 None => None,
84 };
85
86 ctx.start_check(id.clone());
87 RunningCheck {
88 id,
89 bad: false,
90 ctx: self.diag_ctx.clone(),
91 #[cfg(test)]
92 errors: vec![],
93 }
94 }
95
96 pub fn into_failed_checks(self) -> Vec<FinishedCheck> {
97 let ctx = Arc::into_inner(self.diag_ctx).unwrap().into_inner().unwrap();
98 assert!(ctx.running_checks.is_empty(), "Some checks are still running");
99 ctx.finished_checks.into_iter().filter(|c| c.bad).collect()
100 }
101}
102
103fn find_base_commit(tidy_ctx: &TidyCtx) -> Option<String> {
104 let mut check = tidy_ctx.start_check("CI history");
105
106 let stage0 = parse_stage0_file();
107 let Stage0Config { nightly_branch, git_merge_commit_email, .. } = stage0.config;
108
109 let base_commit = match get_closest_upstream_commit(
110 None,
111 &GitConfig {
112 nightly_branch: &nightly_branch,
113 git_merge_commit_email: &git_merge_commit_email,
114 },
115 tidy_ctx.ci_env,
116 ) {
117 Ok(Some(commit)) => Some(commit),
118 Ok(None) => {
119 error_if_in_ci("no base commit found", tidy_ctx.is_running_on_ci(), &mut check);
120 None
121 }
122 Err(error) => {
123 error_if_in_ci(
124 &format!("failed to retrieve base commit: {error}"),
125 tidy_ctx.is_running_on_ci(),
126 &mut check,
127 );
128 None
129 }
130 };
131
132 base_commit
133}
134
135fn error_if_in_ci(msg: &str, is_ci: bool, check: &mut RunningCheck) {
136 if is_ci {
137 check.error(msg);
138 } else {
139 check.warning(format!("{msg}. Some checks will be skipped."));
140 }
141}
142
143struct DiagCtxInner {
144 running_checks: HashSet<CheckId>,
145 finished_checks: HashSet<FinishedCheck>,
146 verbose: bool,
147 root_path: PathBuf,
148}
149
150impl DiagCtxInner {
151 fn start_check(&mut self, id: CheckId) {
152 if self.has_check_id(&id) {
153 panic!("Starting a check named `{id:?}` for the second time");
154 }
155
156 self.running_checks.insert(id);
157 }
158
159 fn finish_check(&mut self, check: FinishedCheck) {
160 assert!(
161 self.running_checks.remove(&check.id),
162 "Finishing check `{:?}` that was not started",
163 check.id
164 );
165
166 if check.bad {
167 output_message("FAIL", Some(&check.id), Some(COLOR_ERROR));
168 } else if self.verbose {
169 output_message("OK", Some(&check.id), Some(COLOR_SUCCESS));
170 }
171
172 self.finished_checks.insert(check);
173 }
174
175 fn has_check_id(&self, id: &CheckId) -> bool {
176 self.running_checks
177 .iter()
178 .chain(self.finished_checks.iter().map(|c| &c.id))
179 .any(|c| c == id)
180 }
181}
182
183#[derive(PartialEq, Eq, Hash, Clone, Debug)]
185pub struct CheckId {
186 pub name: String,
187 pub path: Option<PathBuf>,
188}
189
190impl CheckId {
191 pub fn new(name: &'static str) -> Self {
192 Self { name: name.to_string(), path: None }
193 }
194
195 pub fn path(self, path: &Path) -> Self {
196 Self { path: Some(path.to_path_buf()), ..self }
197 }
198}
199
200impl From<&'static str> for CheckId {
201 fn from(name: &'static str) -> Self {
202 Self::new(name)
203 }
204}
205
206impl Display for CheckId {
207 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
208 write!(f, "{}", self.name)?;
209 if let Some(path) = &self.path {
210 write!(f, " ({})", path.display())?;
211 }
212 Ok(())
213 }
214}
215
216#[derive(PartialEq, Eq, Hash, Debug)]
217pub struct FinishedCheck {
218 id: CheckId,
219 bad: bool,
220}
221
222impl FinishedCheck {
223 pub fn id(&self) -> &CheckId {
224 &self.id
225 }
226}
227
228pub struct RunningCheck {
230 id: CheckId,
231 bad: bool,
232 ctx: Arc<Mutex<DiagCtxInner>>,
233 #[cfg(test)]
234 errors: Vec<String>,
235}
236
237impl RunningCheck {
238 pub fn new_noop() -> Self {
243 let ctx = TidyCtx::new(Path::new(""), false, None, TidyFlags::default());
244 ctx.start_check("noop")
245 }
246
247 pub fn error<T: Display>(&mut self, msg: T) {
249 self.mark_as_bad();
250 let msg = msg.to_string();
251 output_message(&msg, Some(&self.id), Some(COLOR_ERROR));
252 #[cfg(test)]
253 self.errors.push(msg);
254 }
255
256 pub fn warning<T: Display>(&mut self, msg: T) {
258 output_message(&msg.to_string(), Some(&self.id), Some(COLOR_WARNING));
259 }
260
261 pub fn message<T: Display>(&mut self, msg: T) {
263 output_message(&msg.to_string(), Some(&self.id), None);
264 }
265
266 pub fn verbose_msg<T: Display>(&mut self, msg: T) {
268 if self.is_verbose_enabled() {
269 self.message(msg);
270 }
271 }
272
273 pub fn is_bad(&self) -> bool {
275 self.bad
276 }
277
278 pub fn is_verbose_enabled(&self) -> bool {
280 self.ctx.lock().unwrap().verbose
281 }
282
283 #[cfg(test)]
284 pub fn get_errors(&self) -> Vec<String> {
285 self.errors.clone()
286 }
287
288 fn mark_as_bad(&mut self) {
289 self.bad = true;
290 }
291}
292
293impl Drop for RunningCheck {
294 fn drop(&mut self) {
295 self.ctx.lock().unwrap().finish_check(FinishedCheck { id: self.id.clone(), bad: self.bad })
296 }
297}
298
299pub const COLOR_SUCCESS: Color = Color::Green;
300pub const COLOR_ERROR: Color = Color::Red;
301pub const COLOR_WARNING: Color = Color::Yellow;
302
303pub fn output_message(msg: &str, id: Option<&CheckId>, color: Option<Color>) {
306 use termcolor::{ColorChoice, ColorSpec};
307
308 let stderr: &mut dyn termcolor::WriteColor = if cfg!(test) {
309 &mut StderrForUnitTests
310 } else {
311 &mut termcolor::StandardStream::stderr(ColorChoice::Auto)
312 };
313
314 if let Some(color) = &color {
315 stderr.set_color(ColorSpec::new().set_fg(Some(*color))).unwrap();
316 }
317
318 match id {
319 Some(id) => {
320 write!(stderr, "tidy [{}", id.name).unwrap();
321 if let Some(path) = &id.path {
322 write!(stderr, " ({})", path.display()).unwrap();
323 }
324 write!(stderr, "]").unwrap();
325 }
326 None => {
327 write!(stderr, "tidy").unwrap();
328 }
329 }
330 if color.is_some() {
331 stderr.set_color(&ColorSpec::new()).unwrap();
332 }
333
334 writeln!(stderr, ": {msg}").unwrap();
335}
336
337struct StderrForUnitTests;
341
342impl io::Write for StderrForUnitTests {
343 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
344 eprint!("{}", String::from_utf8_lossy(buf));
345 Ok(buf.len())
346 }
347
348 fn flush(&mut self) -> io::Result<()> {
349 Ok(())
350 }
351}
352
353impl termcolor::WriteColor for StderrForUnitTests {
354 fn supports_color(&self) -> bool {
355 false
356 }
357
358 fn set_color(&mut self, _spec: &termcolor::ColorSpec) -> io::Result<()> {
359 Ok(())
360 }
361
362 fn reset(&mut self) -> io::Result<()> {
363 Ok(())
364 }
365}