diff --git a/ethers-solc/src/report/compiler.rs b/ethers-solc/src/report/compiler.rs new file mode 100644 index 00000000..739c98cf --- /dev/null +++ b/ethers-solc/src/report/compiler.rs @@ -0,0 +1,189 @@ +//! Additional logging [CompilerInput] and [CompilerOutput] +//! +//! Useful for debugging purposes. +//! As solc compiler input and output can become quite large (in the tens of MB) we still want a way +//! to get this info when debugging an issue. Most convenient way to look at these object is as a +//! separate json file + +use crate::{CompilerInput, CompilerOutput}; +use std::{env, path::PathBuf, str::FromStr}; + +/// Debug Helper type that can be used to write the [Solc] [CompilerInput] and [CompilerOutput] to +/// disk if configured. +/// +/// # Example +/// +/// If `ETHERS_SOLC_LOG=in=in.json,out=out.json` is then the reporter will be configured to write +/// the compiler input as pretty formatted json to `in.json` and the compiler output to `out.json` +/// +/// ```no_run +/// use ethers_solc::report::SolcCompilerIoReporter; +/// std::env::set_var("ETHERS_SOLC_LOG", "in=in.json,out=out.json"); +/// let rep = SolcCompilerIoReporter::from_default_env(); +/// ``` +#[derive(Debug, Clone, Default)] +pub struct SolcCompilerIoReporter { + /// where to write the output to, `None` if not enabled + target: Option, +} + +impl SolcCompilerIoReporter { + /// Returns a new `SolcCompilerIOLayer` from the fields in the given string, + /// ignoring any that are invalid. + pub fn new(value: impl AsRef) -> Self { + Self { target: Some(value.as_ref().parse().unwrap_or_default()) } + } + + /// `ETHERS_SOLC_LOG` is the default environment variable used by + /// [`SolcCompilerIOLayer::from_default_env`] + /// + /// [`SolcCompilerIOLayer::from_default_env`]: #method.from_default_env + pub const DEFAULT_ENV: &'static str = "ETHERS_SOLC_LOG"; + + /// Returns a new `SolcCompilerIOLayer` from the value of the `ETHERS_SOLC_LOG` environment + /// variable, ignoring any invalid filter directives. + pub fn from_default_env() -> Self { + Self::from_env(Self::DEFAULT_ENV) + } + + /// Returns a new `SolcCompilerIOLayer` from the value of the given environment + /// variable, ignoring any invalid filter directives. + pub fn from_env>(env: A) -> Self { + env::var(env.as_ref()).map(Self::new).unwrap_or_default() + } + + /// Callback to write the input to disk if target is set + pub fn log_compiler_input(&self, input: &CompilerInput) { + if let Some(ref target) = self.target { + target.write_input(input) + } + } + + /// Callback to write the input to disk if target is set + pub fn log_compiler_output(&self, output: &CompilerOutput) { + if let Some(ref target) = self.target { + target.write_output(output) + } + } +} + +impl From for SolcCompilerIoReporter +where + S: AsRef, +{ + fn from(s: S) -> Self { + Self::new(s) + } +} + +/// Represents the `in=,out=` value +#[derive(Debug, Clone, Eq, PartialEq)] +struct Target { + /// path where the compiler input file should be written to + dest_input: PathBuf, + /// path where the compiler output file should be written to + dest_output: PathBuf, +} + +impl Target { + fn write_input(&self, input: &CompilerInput) { + tracing::trace!("logging compiler input to {}", self.dest_input.display()); + match serde_json::to_string_pretty(input) { + Ok(json) => { + if let Err(err) = std::fs::write(&self.dest_input, json) { + tracing::error!("Failed to write compiler input: {}", err) + } + } + Err(err) => { + tracing::error!("Failed to serialize compiler input: {}", err) + } + } + } + + fn write_output(&self, output: &CompilerOutput) { + tracing::trace!("logging compiler output to {}", self.dest_output.display()); + match serde_json::to_string_pretty(output) { + Ok(json) => { + if let Err(err) = std::fs::write(&self.dest_output, json) { + tracing::error!("Failed to write compiler output: {}", err) + } + } + Err(err) => { + tracing::error!("Failed to serialize compiler output: {}", err) + } + } + } +} + +impl Default for Target { + fn default() -> Self { + Self { + dest_input: "compiler-input.json".into(), + dest_output: "compiler-output.json".into(), + } + } +} + +impl FromStr for Target { + type Err = Box; + fn from_str(s: &str) -> Result { + let mut dest_input = None; + let mut dest_output = None; + for part in s.split(',') { + let (name, val) = + part.split_once('=').ok_or_else(|| BadName { name: part.to_string() })?; + match name { + "i" | "in" | "input" | "compilerinput" => { + dest_input = Some(PathBuf::from(val)); + } + "o" | "out" | "output" | "compileroutput" => { + dest_output = Some(PathBuf::from(val)); + } + _ => return Err(BadName { name: part.to_string() }.into()), + }; + } + + Ok(Self { + dest_input: dest_input.unwrap_or_else(|| "compiler-input.json".into()), + dest_output: dest_output.unwrap_or_else(|| "compiler-output.json".into()), + }) + } +} + +/// Indicates that a field name specified in the env value was invalid. +#[derive(Clone, Debug, thiserror::Error)] +#[error("{}", self.name)] +pub struct BadName { + name: String, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn can_parse_target() { + let target: Target = "in=in.json,out=out.json".parse().unwrap(); + assert_eq!(target, Target { dest_input: "in.json".into(), dest_output: "out.json".into() }); + + let target: Target = "in=in.json".parse().unwrap(); + assert_eq!(target, Target { dest_input: "in.json".into(), ..Default::default() }); + + let target: Target = "out=out.json".parse().unwrap(); + assert_eq!(target, Target { dest_output: "out.json".into(), ..Default::default() }); + } + + #[test] + fn can_init_reporter_from_env() { + let rep = SolcCompilerIoReporter::from_default_env(); + assert!(rep.target.is_none()); + std::env::set_var("ETHERS_SOLC_LOG", "in=in.json,out=out.json"); + let rep = SolcCompilerIoReporter::from_default_env(); + assert!(rep.target.is_some()); + assert_eq!( + rep.target.unwrap(), + Target { dest_input: "in.json".into(), dest_output: "out.json".into() } + ); + std::env::remove_var("ETHERS_SOLC_LOG"); + } +} diff --git a/ethers-solc/src/report.rs b/ethers-solc/src/report/mod.rs similarity index 99% rename from ethers-solc/src/report.rs rename to ethers-solc/src/report/mod.rs index 15413012..518ce6b6 100644 --- a/ethers-solc/src/report.rs +++ b/ethers-solc/src/report/mod.rs @@ -29,6 +29,9 @@ use std::{ }, }; +mod compiler; +pub use compiler::SolcCompilerIoReporter; + thread_local! { static CURRENT_STATE: State = State { scoped: RefCell::new(Report::none()),