ethers_solc/report/
compiler.rs

1//! Additional logging [CompilerInput] and [CompilerOutput]
2//!
3//! Useful for debugging purposes.
4//! As solc compiler input and output can become quite large (in the tens of MB) we still want a way
5//! to get this info when debugging an issue. Most convenient way to look at these object is as a
6//! separate json file
7
8use crate::{CompilerInput, CompilerOutput};
9use semver::Version;
10use std::{env, path::PathBuf, str::FromStr};
11
12/// Debug Helper type that can be used to write the [crate::Solc] [CompilerInput] and
13/// [CompilerOutput] to disk if configured.
14///
15/// # Example
16///
17/// If `ETHERS_SOLC_LOG=in=in.json,out=out.json` is then the reporter will be configured to write
18/// the compiler input as pretty formatted json to `in.{solc version}.json` and the compiler output
19/// to `out.{solc version}.json`
20///
21/// ```no_run
22/// use ethers_solc::report::SolcCompilerIoReporter;
23/// std::env::set_var("ETHERS_SOLC_LOG", "in=in.json,out=out.json");
24/// let rep = SolcCompilerIoReporter::from_default_env();
25/// ```
26#[derive(Debug, Clone, Default)]
27pub struct SolcCompilerIoReporter {
28    /// where to write the output to, `None` if not enabled
29    target: Option<Target>,
30}
31
32impl SolcCompilerIoReporter {
33    /// Returns a new `SolcCompilerIOLayer` from the fields in the given string,
34    /// ignoring any that are invalid.
35    pub fn new(value: impl AsRef<str>) -> Self {
36        Self { target: Some(value.as_ref().parse().unwrap_or_default()) }
37    }
38
39    /// `ETHERS_SOLC_LOG` is the default environment variable used by
40    /// [`SolcCompilerIOLayer::from_default_env`]
41    ///
42    /// [`SolcCompilerIOLayer::from_default_env`]: #method.from_default_env
43    pub const DEFAULT_ENV: &'static str = "ETHERS_SOLC_LOG";
44
45    /// Returns a new `SolcCompilerIOLayer` from the value of the `ETHERS_SOLC_LOG` environment
46    /// variable, ignoring any invalid filter directives.
47    pub fn from_default_env() -> Self {
48        Self::from_env(Self::DEFAULT_ENV)
49    }
50
51    /// Returns a new `SolcCompilerIOLayer` from the value of the given environment
52    /// variable, ignoring any invalid filter directives.
53    pub fn from_env<A: AsRef<str>>(env: A) -> Self {
54        env::var(env.as_ref()).map(Self::new).unwrap_or_default()
55    }
56
57    /// Callback to write the input to disk if target is set
58    pub fn log_compiler_input(&self, input: &CompilerInput, version: &Version) {
59        if let Some(ref target) = self.target {
60            target.write_input(input, version)
61        }
62    }
63
64    /// Callback to write the input to disk if target is set
65    pub fn log_compiler_output(&self, output: &CompilerOutput, version: &Version) {
66        if let Some(ref target) = self.target {
67            target.write_output(output, version)
68        }
69    }
70}
71
72impl<S> From<S> for SolcCompilerIoReporter
73where
74    S: AsRef<str>,
75{
76    fn from(s: S) -> Self {
77        Self::new(s)
78    }
79}
80
81/// Represents the `in=<path>,out=<path>` value
82#[derive(Debug, Clone, Eq, PartialEq)]
83struct Target {
84    /// path where the compiler input file should be written to
85    dest_input: PathBuf,
86    /// path where the compiler output file should be written to
87    dest_output: PathBuf,
88}
89
90impl Target {
91    fn write_input(&self, input: &CompilerInput, version: &Version) {
92        tracing::trace!("logging compiler input to {}", self.dest_input.display());
93        match serde_json::to_string_pretty(input) {
94            Ok(json) => {
95                if let Err(err) = std::fs::write(get_file_name(&self.dest_input, version), json) {
96                    tracing::error!("Failed to write compiler input: {}", err)
97                }
98            }
99            Err(err) => {
100                tracing::error!("Failed to serialize compiler input: {}", err)
101            }
102        }
103    }
104
105    fn write_output(&self, output: &CompilerOutput, version: &Version) {
106        tracing::trace!("logging compiler output to {}", self.dest_output.display());
107        match serde_json::to_string_pretty(output) {
108            Ok(json) => {
109                if let Err(err) = std::fs::write(get_file_name(&self.dest_output, version), json) {
110                    tracing::error!("Failed to write compiler output: {}", err)
111                }
112            }
113            Err(err) => {
114                tracing::error!("Failed to serialize compiler output: {}", err)
115            }
116        }
117    }
118}
119
120impl Default for Target {
121    fn default() -> Self {
122        Self {
123            dest_input: "compiler-input.json".into(),
124            dest_output: "compiler-output.json".into(),
125        }
126    }
127}
128
129impl FromStr for Target {
130    type Err = Box<dyn std::error::Error + Send + Sync>;
131    fn from_str(s: &str) -> Result<Self, Self::Err> {
132        let mut dest_input = None;
133        let mut dest_output = None;
134        for part in s.split(',') {
135            let (name, val) =
136                part.split_once('=').ok_or_else(|| BadName { name: part.to_string() })?;
137            match name {
138                "i" | "in" | "input" | "compilerinput" => {
139                    dest_input = Some(PathBuf::from(val));
140                }
141                "o" | "out" | "output" | "compileroutput" => {
142                    dest_output = Some(PathBuf::from(val));
143                }
144                _ => return Err(BadName { name: part.to_string() }.into()),
145            };
146        }
147
148        Ok(Self {
149            dest_input: dest_input.unwrap_or_else(|| "compiler-input.json".into()),
150            dest_output: dest_output.unwrap_or_else(|| "compiler-output.json".into()),
151        })
152    }
153}
154
155/// Indicates that a field name specified in the env value was invalid.
156#[derive(Clone, Debug, thiserror::Error)]
157#[error("{}", self.name)]
158pub struct BadName {
159    name: String,
160}
161
162/// Returns the file name for the given version
163fn get_file_name(path: impl Into<PathBuf>, v: &Version) -> PathBuf {
164    let mut path = path.into();
165    if let Some(stem) = path.file_stem().and_then(|s| s.to_str().map(|s| s.to_string())) {
166        path.set_file_name(format!("{stem}.{}.{}.{}.json", v.major, v.minor, v.patch));
167    }
168    path
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    #[test]
176    fn can_set_file_name() {
177        let s = "/a/b/c/in.json";
178        let p = get_file_name(s, &Version::parse("0.8.10").unwrap());
179        assert_eq!(PathBuf::from("/a/b/c/in.0.8.10.json"), p);
180
181        let s = "abc.json";
182        let p = get_file_name(s, &Version::parse("0.8.10").unwrap());
183        assert_eq!(PathBuf::from("abc.0.8.10.json"), p);
184    }
185
186    #[test]
187    fn can_parse_target() {
188        let target: Target = "in=in.json,out=out.json".parse().unwrap();
189        assert_eq!(target, Target { dest_input: "in.json".into(), dest_output: "out.json".into() });
190
191        let target: Target = "in=in.json".parse().unwrap();
192        assert_eq!(target, Target { dest_input: "in.json".into(), ..Default::default() });
193
194        let target: Target = "out=out.json".parse().unwrap();
195        assert_eq!(target, Target { dest_output: "out.json".into(), ..Default::default() });
196    }
197
198    #[test]
199    fn can_init_reporter_from_env() {
200        let rep = SolcCompilerIoReporter::from_default_env();
201        assert!(rep.target.is_none());
202        std::env::set_var("ETHERS_SOLC_LOG", "in=in.json,out=out.json");
203        let rep = SolcCompilerIoReporter::from_default_env();
204        assert!(rep.target.is_some());
205        assert_eq!(
206            rep.target.unwrap(),
207            Target { dest_input: "in.json".into(), dest_output: "out.json".into() }
208        );
209        std::env::remove_var("ETHERS_SOLC_LOG");
210    }
211}