foundry_compilers/report/
mod.rs1#![allow(static_mut_refs)] use foundry_compilers_artifacts::remappings::Remapping;
20use semver::Version;
21use std::{
22 any::{Any, TypeId},
23 cell::RefCell,
24 error::Error,
25 fmt, io,
26 path::{Path, PathBuf},
27 ptr::NonNull,
28 sync::{
29 Arc,
30 atomic::{AtomicBool, AtomicUsize, Ordering},
31 },
32 time::Duration,
33};
34
35mod compiler;
36pub use compiler::SolcCompilerIoReporter;
37
38thread_local! {
39 static CURRENT_STATE: State = State {
40 scoped: RefCell::new(Report::none()),
41 };
42}
43
44static EXISTS: AtomicBool = AtomicBool::new(false);
45static SCOPED_COUNT: AtomicUsize = AtomicUsize::new(0);
46
47static GLOBAL_REPORTER_STATE: AtomicUsize = AtomicUsize::new(UN_SET);
49
50const UN_SET: usize = 0;
51const SETTING: usize = 1;
52const SET: usize = 2;
53
54static mut GLOBAL_REPORTER: Option<Report> = None;
55
56pub fn try_init<T>(reporter: T) -> Result<(), Box<dyn Error + Send + Sync + 'static>>
64where
65 T: Reporter + Send + Sync + 'static,
66{
67 set_global_reporter(Report::new(reporter))?;
68 Ok(())
69}
70
71pub fn init<T>(reporter: T)
82where
83 T: Reporter + Send + Sync + 'static,
84{
85 try_init(reporter).expect("Failed to install global reporter")
86}
87
88pub trait Reporter: 'static + std::fmt::Debug {
99 fn on_compiler_spawn(
107 &self,
108 _compiler_name: &str,
109 _version: &Version,
110 _dirty_files: &[PathBuf],
111 ) {
112 }
113
114 fn on_compiler_success(&self, _compiler_name: &str, _version: &Version, _duration: &Duration) {}
118
119 fn on_solc_installation_start(&self, _version: &Version) {}
121
122 fn on_solc_installation_success(&self, _version: &Version) {}
124
125 fn on_solc_installation_error(&self, _version: &Version, _error: &str) {}
127
128 fn on_unresolved_imports(&self, _imports: &[(&Path, &Path)], _remappings: &[Remapping]) {}
131
132 unsafe fn downcast_raw(&self, id: TypeId) -> Option<NonNull<()>> {
151 (id == TypeId::of::<Self>()).then(|| NonNull::from(self).cast())
152 }
153}
154
155impl dyn Reporter {
156 pub fn is<T: Any>(&self) -> bool {
158 self.downcast_ref::<T>().is_some()
159 }
160
161 pub fn downcast_ref<T: Any>(&self) -> Option<&T> {
164 unsafe {
165 let raw = self.downcast_raw(TypeId::of::<T>())?;
166 Some(&*(raw.cast().as_ptr()))
167 }
168 }
169}
170
171pub(crate) fn compiler_spawn(compiler_name: &str, version: &Version, dirty_files: &[PathBuf]) {
172 get_default(|r| r.reporter.on_compiler_spawn(compiler_name, version, dirty_files));
173}
174
175pub(crate) fn compiler_success(compiler_name: &str, version: &Version, duration: &Duration) {
176 get_default(|r| r.reporter.on_compiler_success(compiler_name, version, duration));
177}
178
179#[allow(dead_code)]
180pub(crate) fn solc_installation_start(version: &Version) {
181 get_default(|r| r.reporter.on_solc_installation_start(version));
182}
183
184#[allow(dead_code)]
185pub(crate) fn solc_installation_success(version: &Version) {
186 get_default(|r| r.reporter.on_solc_installation_success(version));
187}
188
189#[allow(dead_code)]
190pub(crate) fn solc_installation_error(version: &Version, error: &str) {
191 get_default(|r| r.reporter.on_solc_installation_error(version, error));
192}
193
194pub(crate) fn unresolved_imports(imports: &[(&Path, &Path)], remappings: &[Remapping]) {
195 get_default(|r| r.reporter.on_unresolved_imports(imports, remappings));
196}
197
198fn get_global() -> Option<&'static Report> {
199 if GLOBAL_REPORTER_STATE.load(Ordering::SeqCst) != SET {
200 return None;
201 }
202 unsafe {
203 Some(GLOBAL_REPORTER.as_ref().expect(
206 "Reporter invariant violated: GLOBAL_REPORTER must be initialized before GLOBAL_REPORTER_STATE is set",
207 ))
208 }
209}
210
211#[inline(always)]
213pub fn get_default<T, F>(mut f: F) -> T
214where
215 F: FnMut(&Report) -> T,
216{
217 if SCOPED_COUNT.load(Ordering::Acquire) == 0 {
218 return if let Some(glob) = get_global() { f(glob) } else { f(&Report::none()) };
221 }
222
223 get_default_scoped(f)
224}
225
226#[inline(never)]
227fn get_default_scoped<T, F>(mut f: F) -> T
228where
229 F: FnMut(&Report) -> T,
230{
231 CURRENT_STATE
232 .try_with(|state| {
233 let scoped = state.scoped.borrow_mut();
234 f(&scoped)
235 })
236 .unwrap_or_else(|_| f(&Report::none()))
237}
238
239pub fn with_global<T>(f: impl FnOnce(&Report) -> T) -> Option<T> {
241 let report = get_global()?;
242 Some(f(report))
243}
244
245pub fn with_scoped<T>(report: &Report, f: impl FnOnce() -> T) -> T {
247 let _guard = set_scoped(report);
252 f()
253}
254
255struct State {
257 scoped: RefCell<Report>,
259}
260
261impl State {
262 #[inline]
268 fn set_scoped(new_report: Report) -> ScopeGuard {
269 let prior = CURRENT_STATE.try_with(|state| state.scoped.replace(new_report)).ok();
270 EXISTS.store(true, Ordering::Release);
271 SCOPED_COUNT.fetch_add(1, Ordering::Release);
272 ScopeGuard(prior)
273 }
274}
275
276#[derive(Debug)]
279pub struct ScopeGuard(Option<Report>);
280
281impl Drop for ScopeGuard {
282 #[inline]
283 fn drop(&mut self) {
284 SCOPED_COUNT.fetch_sub(1, Ordering::Release);
285 if let Some(report) = self.0.take() {
286 let prev = CURRENT_STATE.try_with(|state| state.scoped.replace(report));
289 drop(prev)
290 }
291 }
292}
293
294#[must_use = "Dropping the guard unregisters the reporter."]
297pub fn set_scoped(reporter: &Report) -> ScopeGuard {
298 State::set_scoped(reporter.clone())
302}
303
304#[derive(Clone, Copy, Debug, Default)]
306pub struct NoReporter(());
307
308impl Reporter for NoReporter {}
309
310#[derive(Clone, Debug, Default)]
316pub struct BasicStdoutReporter {
317 _priv: (),
318}
319
320impl Reporter for BasicStdoutReporter {
321 fn on_compiler_spawn(&self, compiler_name: &str, version: &Version, dirty_files: &[PathBuf]) {
325 write_line(
326 io::stdout().lock(),
327 format_args!(
328 "Compiling {} files with {} {}.{}.{}",
329 dirty_files.len(),
330 compiler_name,
331 version.major,
332 version.minor,
333 version.patch
334 ),
335 );
336 }
337
338 fn on_compiler_success(&self, compiler_name: &str, version: &Version, duration: &Duration) {
339 write_line(
340 io::stdout().lock(),
341 format_args!(
342 "{} {}.{}.{} finished in {duration:.2?}",
343 compiler_name, version.major, version.minor, version.patch
344 ),
345 );
346 }
347
348 fn on_solc_installation_start(&self, version: &Version) {
350 write_line(io::stdout().lock(), format_args!("installing solc version \"{version}\""));
351 }
352
353 fn on_solc_installation_success(&self, version: &Version) {
355 write_line(io::stdout().lock(), format_args!("Successfully installed solc {version}"));
356 }
357
358 fn on_solc_installation_error(&self, version: &Version, error: &str) {
359 write_line(io::stderr().lock(), format_args!("Failed to install solc {version}: {error}"));
360 }
361
362 fn on_unresolved_imports(&self, imports: &[(&Path, &Path)], remappings: &[Remapping]) {
363 if imports.is_empty() {
364 return;
365 }
366 write_line(
367 io::stdout().lock(),
368 format_args!("{}", format_unresolved_imports(imports, remappings)),
369 );
370 }
371}
372
373fn write_line(mut writer: impl io::Write, args: fmt::Arguments<'_>) {
377 if let Err(err) = writeln!(writer, "{args}")
378 && err.kind() != io::ErrorKind::BrokenPipe
379 {
380 panic!("failed to write reporter output: {err}");
381 }
382}
383
384pub fn format_unresolved_imports(imports: &[(&Path, &Path)], remappings: &[Remapping]) -> String {
386 let info = imports
387 .iter()
388 .map(|(import, file)| format!("\"{}\" in \"{}\"", import.display(), file.display()))
389 .collect::<Vec<_>>()
390 .join("\n ");
391 format!(
392 "Unable to resolve imports:\n {}\nwith remappings:\n {}",
393 info,
394 remappings.iter().map(|r| r.to_string()).collect::<Vec<_>>().join("\n ")
395 )
396}
397
398#[derive(Debug)]
400pub struct SetGlobalReporterError {
401 _priv: (),
403}
404
405impl fmt::Display for SetGlobalReporterError {
406 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
407 f.pad("a global reporter has already been set")
408 }
409}
410
411impl Error for SetGlobalReporterError {}
412
413#[derive(Clone)]
415pub struct Report {
416 reporter: Arc<dyn Reporter + Send + Sync>,
417}
418
419impl Report {
420 pub fn none() -> Self {
422 Self { reporter: Arc::new(NoReporter::default()) }
423 }
424
425 pub fn new<S>(reporter: S) -> Self
429 where
430 S: Reporter + Send + Sync + 'static,
431 {
432 Self { reporter: Arc::new(reporter) }
433 }
434
435 #[inline]
438 pub fn is<T: Any>(&self) -> bool {
439 <dyn Reporter>::is::<T>(&*self.reporter)
440 }
441}
442
443impl fmt::Debug for Report {
444 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
445 f.pad("Report(...)")
446 }
447}
448
449fn set_global_reporter(report: Report) -> Result<(), SetGlobalReporterError> {
454 if GLOBAL_REPORTER_STATE
458 .compare_exchange(UN_SET, SETTING, Ordering::SeqCst, Ordering::SeqCst)
459 .is_ok()
460 {
461 unsafe {
462 GLOBAL_REPORTER = Some(report);
463 }
464 GLOBAL_REPORTER_STATE.store(SET, Ordering::SeqCst);
465 Ok(())
466 } else {
467 Err(SetGlobalReporterError { _priv: () })
468 }
469}
470
471#[cfg(test)]
472mod tests {
473 use super::*;
474 use std::str::FromStr;
475
476 #[test]
477 fn scoped_reporter_works() {
478 #[derive(Debug)]
479 struct TestReporter;
480 impl Reporter for TestReporter {}
481
482 with_scoped(&Report::new(TestReporter), || {
483 get_default(|reporter| assert!(reporter.is::<TestReporter>()))
484 });
485 }
486
487 #[test]
488 fn global_and_scoped_reporter_works() {
489 get_default(|reporter| {
490 assert!(reporter.is::<NoReporter>());
491 });
492
493 set_global_reporter(Report::new(BasicStdoutReporter::default())).unwrap();
494 #[derive(Debug)]
495 struct TestReporter;
496 impl Reporter for TestReporter {}
497
498 with_scoped(&Report::new(TestReporter), || {
499 get_default(|reporter| assert!(reporter.is::<TestReporter>()))
500 });
501
502 get_default(|reporter| assert!(reporter.is::<BasicStdoutReporter>()))
503 }
504
505 #[test]
506 fn write_line_ignores_broken_pipe() {
507 struct BrokenPipeWriter;
508
509 impl io::Write for BrokenPipeWriter {
510 fn write(&mut self, _buf: &[u8]) -> io::Result<usize> {
511 Err(io::Error::new(io::ErrorKind::BrokenPipe, "broken pipe"))
512 }
513
514 fn flush(&mut self) -> io::Result<()> {
515 Ok(())
516 }
517 }
518
519 write_line(BrokenPipeWriter, format_args!("hello"));
521 }
522
523 #[test]
524 #[should_panic(expected = "failed to write reporter output")]
525 fn write_line_panics_on_non_broken_pipe_errors() {
526 struct FailingWriter;
527
528 impl io::Write for FailingWriter {
529 fn write(&mut self, _buf: &[u8]) -> io::Result<usize> {
530 Err(io::Error::other("write failed"))
531 }
532
533 fn flush(&mut self) -> io::Result<()> {
534 Ok(())
535 }
536 }
537
538 write_line(FailingWriter, format_args!("hello"));
539 }
540
541 #[test]
542 fn write_line_writes_newline_terminated_output() {
543 #[derive(Clone, Default)]
544 struct BufferWriter(std::sync::Arc<std::sync::Mutex<Vec<u8>>>);
545
546 impl io::Write for BufferWriter {
547 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
548 self.0.lock().unwrap().extend_from_slice(buf);
549 Ok(buf.len())
550 }
551
552 fn flush(&mut self) -> io::Result<()> {
553 Ok(())
554 }
555 }
556
557 let writer = BufferWriter::default();
558 let buffer = writer.0.clone();
559 write_line(writer, format_args!("hello"));
560
561 assert_eq!(String::from_utf8(buffer.lock().unwrap().clone()).unwrap(), "hello\n");
562 }
563
564 #[test]
565 fn test_unresolved_message() {
566 let unresolved = vec![(Path::new("./src/Import.sol"), Path::new("src/File.col"))];
567
568 let remappings = vec![Remapping::from_str("oz=a/b/c/d").unwrap()];
569
570 assert_eq!(
571 format_unresolved_imports(&unresolved, &remappings).trim(),
572 r#"
573Unable to resolve imports:
574 "./src/Import.sol" in "src/File.col"
575with remappings:
576 oz/=a/b/c/d/"#
577 .trim()
578 )
579 }
580}