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