foundry_compilers/
buildinfo.rs

1//! Represents an entire build
2
3use crate::compilers::{
4    CompilationError, CompilerContract, CompilerInput, CompilerOutput, Language,
5};
6use alloy_primitives::hex;
7use foundry_compilers_core::{error::Result, utils};
8use md5::Digest;
9use semver::Version;
10use serde::{de::DeserializeOwned, Deserialize, Serialize};
11use std::{
12    collections::{BTreeMap, HashSet},
13    path::{Path, PathBuf},
14};
15
16pub const ETHERS_FORMAT_VERSION: &str = "ethers-rs-sol-build-info-1";
17
18// A hardhat compatible build info representation
19#[derive(Serialize, Deserialize)]
20#[serde(rename_all = "camelCase")]
21pub struct BuildInfo<I, O> {
22    pub id: String,
23    #[serde(rename = "_format")]
24    pub format: String,
25    pub solc_version: Version,
26    pub solc_long_version: Version,
27    pub input: I,
28    pub output: O,
29}
30
31impl<I: DeserializeOwned, O: DeserializeOwned> BuildInfo<I, O> {
32    /// Deserializes the `BuildInfo` object from the given file
33    pub fn read(path: &Path) -> Result<Self> {
34        utils::read_json_file(path)
35    }
36}
37
38/// Additional context we cache for each compiler run.
39#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
40pub struct BuildContext<L> {
41    /// Mapping from internal compiler source id to path of the source file.
42    pub source_id_to_path: BTreeMap<u32, PathBuf>,
43    /// Language of the compiler.
44    pub language: L,
45}
46
47impl<L: Language> BuildContext<L> {
48    pub fn new<I, E, C>(input: &I, output: &CompilerOutput<E, C>) -> Result<Self>
49    where
50        I: CompilerInput<Language = L>,
51    {
52        let mut source_id_to_path = BTreeMap::new();
53
54        let input_sources = input.sources().map(|(path, _)| path).collect::<HashSet<_>>();
55        for (path, source) in output.sources.iter() {
56            if input_sources.contains(path.as_path()) {
57                source_id_to_path.insert(source.id, path.to_path_buf());
58            }
59        }
60
61        Ok(Self { source_id_to_path, language: input.language() })
62    }
63
64    pub fn join_all(&mut self, root: &Path) {
65        self.source_id_to_path.values_mut().for_each(|path| {
66            *path = root.join(path.as_path());
67        });
68    }
69
70    pub fn with_joined_paths(mut self, root: &Path) -> Self {
71        self.join_all(root);
72        self
73    }
74}
75
76/// Represents `BuildInfo` object
77#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
78pub struct RawBuildInfo<L> {
79    /// The hash that identifies the BuildInfo
80    pub id: String,
81    #[serde(flatten)]
82    pub build_context: BuildContext<L>,
83    /// serialized `BuildInfo` json
84    #[serde(flatten)]
85    pub build_info: BTreeMap<String, serde_json::Value>,
86}
87
88// === impl RawBuildInfo ===
89
90impl<L: Language> RawBuildInfo<L> {
91    /// Serializes a `BuildInfo` object
92    pub fn new<I: CompilerInput<Language = L>, E: CompilationError, C: CompilerContract>(
93        input: &I,
94        output: &CompilerOutput<E, C>,
95        full_build_info: bool,
96    ) -> Result<Self> {
97        let version = input.version().clone();
98        let build_context = BuildContext::new(input, output)?;
99
100        let mut hasher = md5::Md5::new();
101
102        hasher.update(ETHERS_FORMAT_VERSION);
103
104        let solc_short = format!("{}.{}.{}", version.major, version.minor, version.patch);
105        hasher.update(&solc_short);
106        hasher.update(version.to_string());
107
108        let input = serde_json::to_value(input)?;
109        hasher.update(&serde_json::to_string(&input)?);
110
111        // create the hash for `{_format,solcVersion,solcLongVersion,input}`
112        // N.B. this is not exactly the same as hashing the json representation of these values but
113        // the must efficient one
114        let result = hasher.finalize();
115        let id = hex::encode(result);
116
117        let mut build_info = BTreeMap::new();
118
119        if full_build_info {
120            build_info.insert("_format".to_string(), serde_json::to_value(ETHERS_FORMAT_VERSION)?);
121            build_info.insert("solcVersion".to_string(), serde_json::to_value(&solc_short)?);
122            build_info.insert("solcLongVersion".to_string(), serde_json::to_value(&version)?);
123            build_info.insert("input".to_string(), input);
124            build_info.insert("output".to_string(), serde_json::to_value(output)?);
125        }
126
127        Ok(Self { id, build_info, build_context })
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134    use crate::compilers::solc::SolcVersionedInput;
135    use foundry_compilers_artifacts::{sources::Source, Contract, Error, SolcLanguage, Sources};
136    use std::path::PathBuf;
137
138    #[test]
139    fn build_info_serde() {
140        let v: Version = "0.8.4+commit.c7e474f2".parse().unwrap();
141        let input = SolcVersionedInput::build(
142            Sources::from([(PathBuf::from("input.sol"), Source::new(""))]),
143            Default::default(),
144            SolcLanguage::Solidity,
145            v,
146        );
147        let output = CompilerOutput::<Error, Contract>::default();
148        let raw_info = RawBuildInfo::new(&input, &output, true).unwrap();
149        let _info: BuildInfo<SolcVersionedInput, CompilerOutput<Error, Contract>> =
150            serde_json::from_str(&serde_json::to_string(&raw_info).unwrap()).unwrap();
151    }
152}