1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
//! Represents an entire build

use crate::compilers::{CompilationError, CompilerInput, CompilerOutput, Language};
use alloy_primitives::hex;
use foundry_compilers_core::{error::Result, utils};
use md5::Digest;
use semver::Version;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use std::{
    collections::{BTreeMap, HashSet},
    path::{Path, PathBuf},
};

pub const ETHERS_FORMAT_VERSION: &str = "ethers-rs-sol-build-info-1";

// A hardhat compatible build info representation
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BuildInfo<I, O> {
    pub id: String,
    #[serde(rename = "_format")]
    pub format: String,
    pub solc_version: Version,
    pub solc_long_version: Version,
    pub input: I,
    pub output: O,
}

impl<I: DeserializeOwned, O: DeserializeOwned> BuildInfo<I, O> {
    /// Deserializes the `BuildInfo` object from the given file
    pub fn read(path: &Path) -> Result<Self> {
        utils::read_json_file(path)
    }
}

/// Additional context we cache for each compiler run.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct BuildContext<L> {
    /// Mapping from internal compiler source id to path of the source file.
    pub source_id_to_path: BTreeMap<u32, PathBuf>,
    /// Language of the compiler.
    pub language: L,
}

impl<L: Language> BuildContext<L> {
    pub fn new<I, E>(input: &I, output: &CompilerOutput<E>) -> Result<Self>
    where
        I: CompilerInput<Language = L>,
    {
        let mut source_id_to_path = BTreeMap::new();

        let input_sources = input.sources().map(|(path, _)| path).collect::<HashSet<_>>();
        for (path, source) in output.sources.iter() {
            if input_sources.contains(path.as_path()) {
                source_id_to_path.insert(source.id, path.to_path_buf());
            }
        }

        Ok(Self { source_id_to_path, language: input.language() })
    }

    pub fn join_all(&mut self, root: &Path) {
        self.source_id_to_path.values_mut().for_each(|path| {
            *path = root.join(path.as_path());
        });
    }

    pub fn with_joined_paths(mut self, root: &Path) -> Self {
        self.join_all(root);
        self
    }
}

/// Represents `BuildInfo` object
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RawBuildInfo<L> {
    /// The hash that identifies the BuildInfo
    pub id: String,
    #[serde(flatten)]
    pub build_context: BuildContext<L>,
    /// serialized `BuildInfo` json
    #[serde(flatten)]
    pub build_info: BTreeMap<String, serde_json::Value>,
}

// === impl RawBuildInfo ===

impl<L: Language> RawBuildInfo<L> {
    /// Serializes a `BuildInfo` object
    pub fn new<I: CompilerInput<Language = L>, E: CompilationError>(
        input: &I,
        output: &CompilerOutput<E>,
        full_build_info: bool,
    ) -> Result<Self> {
        let version = input.version().clone();
        let build_context = BuildContext::new(input, output)?;

        let mut hasher = md5::Md5::new();

        hasher.update(ETHERS_FORMAT_VERSION);

        let solc_short = format!("{}.{}.{}", version.major, version.minor, version.patch);
        hasher.update(&solc_short);
        hasher.update(version.to_string());

        let input = serde_json::to_value(input)?;
        hasher.update(&serde_json::to_string(&input)?);

        // create the hash for `{_format,solcVersion,solcLongVersion,input}`
        // N.B. this is not exactly the same as hashing the json representation of these values but
        // the must efficient one
        let result = hasher.finalize();
        let id = hex::encode(result);

        let mut build_info = BTreeMap::new();

        if full_build_info {
            build_info.insert("_format".to_string(), serde_json::to_value(ETHERS_FORMAT_VERSION)?);
            build_info.insert("solcVersion".to_string(), serde_json::to_value(&solc_short)?);
            build_info.insert("solcLongVersion".to_string(), serde_json::to_value(&version)?);
            build_info.insert("input".to_string(), input);
            build_info.insert("output".to_string(), serde_json::to_value(output)?);
        }

        Ok(Self { id, build_info, build_context })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::compilers::solc::SolcVersionedInput;
    use foundry_compilers_artifacts::{sources::Source, Error, SolcLanguage, Sources};
    use std::path::PathBuf;

    #[test]
    fn build_info_serde() {
        let v: Version = "0.8.4+commit.c7e474f2".parse().unwrap();
        let input = SolcVersionedInput::build(
            Sources::from([(PathBuf::from("input.sol"), Source::new(""))]),
            Default::default(),
            SolcLanguage::Solidity,
            v,
        );
        let output = CompilerOutput::<Error>::default();
        let raw_info = RawBuildInfo::new(&input, &output, true).unwrap();
        let _info: BuildInfo<SolcVersionedInput, CompilerOutput<Error>> =
            serde_json::from_str(&serde_json::to_string(&raw_info).unwrap()).unwrap();
    }
}