Skip to main content

foundry_compilers/report/
compiler.rs

1//! Additional logging [SolcInput] 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 foundry_compilers_artifacts::{CompilerOutput, SolcInput};
9use semver::Version;
10use std::{env, path::PathBuf, str::FromStr};
11
12/// Debug Helper type that can be used to write the [crate::compilers::solc::Solc] [SolcInput] and
13/// [CompilerOutput] to disk if configured.
14///
15/// # Examples
16///
17/// If `foundry_compilers_LOG=in=in.json,out=out.json` is then the reporter will be configured to
18/// write the compiler input as pretty formatted json to `in.{solc version}.json` and the compiler
19/// output to `out.{solc version}.json`
20///
21/// ```no_run
22/// use foundry_compilers::report::SolcCompilerIoReporter;
23/// let rep = SolcCompilerIoReporter::new("in=in.json,out=out.json");
24/// ```
25#[derive(Clone, Debug, Default)]
26pub struct SolcCompilerIoReporter {
27    /// where to write the output to, `None` if not enabled
28    target: Option<Target>,
29}
30
31impl SolcCompilerIoReporter {
32    /// Returns a new `SolcCompilerIOLayer` from the fields in the given string,
33    /// ignoring any that are invalid.
34    pub fn new(value: &str) -> Self {
35        Self { target: Some(value.parse().unwrap_or_default()) }
36    }
37
38    /// `foundry_compilers_LOG` is the default environment variable used by
39    /// [`SolcCompilerIOLayer::from_default_env`]
40    ///
41    /// [`SolcCompilerIOLayer::from_default_env`]: #method.from_default_env
42    pub const DEFAULT_ENV: &'static str = "foundry_compilers_LOG";
43
44    /// Returns a new `SolcCompilerIOLayer` from the value of the `foundry_compilers_LOG`
45    /// environment variable, ignoring any invalid filter directives.
46    pub fn from_default_env() -> Self {
47        Self::from_env(Self::DEFAULT_ENV)
48    }
49
50    /// Returns a new `SolcCompilerIOLayer` from the value of the given environment
51    /// variable, ignoring any invalid filter directives.
52    pub fn from_env(env: impl AsRef<std::ffi::OsStr>) -> Self {
53        env::var(env).map(|var| Self::new(&var)).unwrap_or_default()
54    }
55
56    /// Callback to write the input to disk if target is set
57    pub fn log_compiler_input(&self, input: &SolcInput, version: &Version) {
58        if let Some(target) = &self.target {
59            target.write_input(input, version)
60        }
61    }
62
63    /// Callback to write the input to disk if target is set
64    pub fn log_compiler_output(&self, output: &CompilerOutput, version: &Version) {
65        if let Some(target) = &self.target {
66            target.write_output(output, version)
67        }
68    }
69}
70
71impl<S: AsRef<str>> From<S> for SolcCompilerIoReporter {
72    fn from(s: S) -> Self {
73        Self::new(s.as_ref())
74    }
75}
76
77/// Represents the `in=<path>,out=<path>` value
78#[derive(Clone, Debug, PartialEq, Eq)]
79struct Target {
80    /// path where the compiler input file should be written to
81    dest_input: PathBuf,
82    /// path where the compiler output file should be written to
83    dest_output: PathBuf,
84}
85
86impl Target {
87    fn write_input(&self, input: &SolcInput, version: &Version) {
88        trace!("logging compiler input to {}", self.dest_input.display());
89        match serde_json::to_string_pretty(input) {
90            Ok(json) => {
91                if let Err(err) = std::fs::write(get_file_name(&self.dest_input, version), json) {
92                    error!("Failed to write compiler input: {}", err)
93                }
94            }
95            Err(err) => {
96                error!("Failed to serialize compiler input: {}", err)
97            }
98        }
99    }
100
101    fn write_output(&self, output: &CompilerOutput, version: &Version) {
102        trace!("logging compiler output to {}", self.dest_output.display());
103        match serde_json::to_string_pretty(output) {
104            Ok(json) => {
105                if let Err(err) = std::fs::write(get_file_name(&self.dest_output, version), json) {
106                    error!("Failed to write compiler output: {}", err)
107                }
108            }
109            Err(err) => {
110                error!("Failed to serialize compiler output: {}", err)
111            }
112        }
113    }
114}
115
116impl Default for Target {
117    fn default() -> Self {
118        Self {
119            dest_input: "compiler-input.json".into(),
120            dest_output: "compiler-output.json".into(),
121        }
122    }
123}
124
125impl FromStr for Target {
126    type Err = Box<dyn std::error::Error + Send + Sync>;
127    fn from_str(s: &str) -> Result<Self, Self::Err> {
128        let mut dest_input = None;
129        let mut dest_output = None;
130        for part in s.split(',') {
131            let (name, val) =
132                part.split_once('=').ok_or_else(|| BadName { name: part.to_string() })?;
133            match name {
134                "i" | "in" | "input" | "compilerinput" => {
135                    dest_input = Some(PathBuf::from(val));
136                }
137                "o" | "out" | "output" | "compileroutput" => {
138                    dest_output = Some(PathBuf::from(val));
139                }
140                _ => return Err(BadName { name: part.to_string() }.into()),
141            };
142        }
143
144        Ok(Self {
145            dest_input: dest_input.unwrap_or_else(|| "compiler-input.json".into()),
146            dest_output: dest_output.unwrap_or_else(|| "compiler-output.json".into()),
147        })
148    }
149}
150
151/// Indicates that a field name specified in the env value was invalid.
152#[derive(Clone, Debug, thiserror::Error)]
153#[error("{}", self.name)]
154pub struct BadName {
155    name: String,
156}
157
158/// Returns the file name for the given version
159fn get_file_name(path: impl Into<PathBuf>, v: &Version) -> PathBuf {
160    let mut path = path.into();
161    if let Some(stem) = path.file_stem().and_then(|s| s.to_str().map(|s| s.to_string())) {
162        path.set_file_name(format!("{stem}.{}.{}.{}.json", v.major, v.minor, v.patch));
163    }
164    path
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170    use std::fs;
171    use tempfile::tempdir;
172
173    #[test]
174    fn can_set_file_name() {
175        let s = "/a/b/c/in.json";
176        let p = get_file_name(s, &Version::new(0, 8, 10));
177        assert_eq!(PathBuf::from("/a/b/c/in.0.8.10.json"), p);
178
179        let s = "abc.json";
180        let p = get_file_name(s, &Version::new(0, 8, 10));
181        assert_eq!(PathBuf::from("abc.0.8.10.json"), p);
182    }
183
184    #[test]
185    fn can_parse_target() {
186        let target: Target = "in=in.json,out=out.json".parse().unwrap();
187        assert_eq!(target, Target { dest_input: "in.json".into(), dest_output: "out.json".into() });
188
189        let target: Target = "in=in.json".parse().unwrap();
190        assert_eq!(target, Target { dest_input: "in.json".into(), ..Default::default() });
191
192        let target: Target = "out=out.json".parse().unwrap();
193        assert_eq!(target, Target { dest_output: "out.json".into(), ..Default::default() });
194    }
195
196    #[test]
197    fn can_init_reporter_from_env() {
198        let rep = SolcCompilerIoReporter::from_default_env();
199        assert!(rep.target.is_none());
200        // SAFETY: This test is not run in parallel with other tests that depend on this env var.
201        unsafe { std::env::set_var("foundry_compilers_LOG", "in=in.json,out=out.json") };
202        let rep = SolcCompilerIoReporter::from_default_env();
203        assert!(rep.target.is_some());
204        assert_eq!(
205            rep.target.unwrap(),
206            Target { dest_input: "in.json".into(), dest_output: "out.json".into() }
207        );
208        // SAFETY: This test is not run in parallel with other tests that depend on this env var.
209        unsafe { std::env::remove_var("foundry_compilers_LOG") };
210    }
211
212    #[test]
213    fn check_no_write_when_no_target() {
214        let reporter = SolcCompilerIoReporter::default();
215        let version = Version::parse("0.8.10").unwrap();
216        let input = SolcInput::default();
217        let output = CompilerOutput::default();
218
219        reporter.log_compiler_input(&input, &version);
220        reporter.log_compiler_output(&output, &version);
221    }
222
223    #[test]
224    fn serialize_and_write_to_file() {
225        let dir = tempdir().unwrap();
226        let input_path = dir.path().join("input.json");
227        let output_path = dir.path().join("output.json");
228        let version = Version::parse("0.8.10").unwrap();
229        let target = Target { dest_input: input_path.clone(), dest_output: output_path.clone() };
230
231        let input = SolcInput::default();
232        let output = CompilerOutput::default();
233
234        target.write_input(&input, &version);
235        target.write_output(&output, &version);
236
237        let input_content = fs::read_to_string(get_file_name(&input_path, &version)).unwrap();
238        let output_content = fs::read_to_string(get_file_name(&output_path, &version)).unwrap();
239
240        assert!(!input_content.is_empty());
241        assert!(!output_content.is_empty());
242
243        dir.close().unwrap();
244    }
245}