From 1da62d65d2f8f4110a350cad9e23f02bf9f0ce78 Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Tue, 16 Nov 2021 00:29:06 +0100 Subject: [PATCH] refactor!: make artifactoutput a trait (#579) * feat: add artifacts trait * add artifactsoutput trait * deprecated old artifactoutput * feat: better artifacts handling * force update * feat: update metadata artifacts * feat: add default to types * feat: make useliteralcontent optional * feat: replace ProjectCompilerOutput with struct * docs * add output function * feat: add Artifact trait for reading Abi/Bytes from an artifact * feat(solc): replace () for empty artifacts with a conditional check As discussed with @mattsse the abstraction here might not be super clean, so we should revisit this if we do not like it * chore: fix doctest Co-authored-by: Georgios Konstantopoulos --- ethers-solc/src/artifacts.rs | 147 ++++++- ethers-solc/src/cache.rs | 71 ++- ethers-solc/src/config.rs | 223 +++++++--- ethers-solc/src/lib.rs | 407 ++++++++++++++---- ethers-solc/src/remappings.rs | 2 +- .../test-data/dapp-sample/src/Dapp.sol | 2 +- .../test-data/dapp-sample/src/Dapp.t.sol | 2 +- .../hardhat-sample/contracts/Greeter.sol | 2 +- ethers-solc/test-data/out/compiler-out-1.json | 1 - ethers-solc/tests/project.rs | 41 +- 10 files changed, 713 insertions(+), 185 deletions(-) diff --git a/ethers-solc/src/artifacts.rs b/ethers-solc/src/artifacts.rs index c881b3ea..63ab7ec7 100644 --- a/ethers-solc/src/artifacts.rs +++ b/ethers-solc/src/artifacts.rs @@ -76,7 +76,7 @@ pub struct Settings { pub remappings: Vec, pub optimizer: Optimizer, #[serde(default, skip_serializing_if = "Option::is_none")] - pub metadata: Option, + pub metadata: Option, /// This field can be used to select desired outputs based /// on file and contract names. /// If this field is omitted, then the compiler loads and does type @@ -294,11 +294,76 @@ impl FromStr for EvmVersion { } } } +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct SettingsMetadata { + #[serde(default, rename = "useLiteralContent", skip_serializing_if = "Option::is_none")] + pub use_literal_content: Option, + #[serde(default, rename = "bytecodeHash", skip_serializing_if = "Option::is_none")] + pub bytecode_hash: Option, +} #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct Metadata { - #[serde(rename = "useLiteralContent")] - pub use_literal_content: bool, + pub compiler: Compiler, + pub language: String, + pub output: Output, + pub settings: Settings, + pub sources: MetadataSources, + pub version: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct MetadataSources { + #[serde(flatten)] + pub inner: BTreeMap, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct Compiler { + pub version: String, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct Output { + pub abi: Vec, + pub devdoc: Option, + pub userdoc: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct SolcAbi { + pub inputs: Vec, + #[serde(rename = "stateMutability")] + pub state_mutability: Option, + #[serde(rename = "type")] + pub abi_type: String, + pub name: Option, + pub outputs: Option>, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct Item { + #[serde(rename = "internalType")] + pub internal_type: String, + pub name: String, + #[serde(rename = "type")] + pub put_type: String, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct Doc { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub kind: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub methods: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub version: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)] +pub struct Libraries { + #[serde(flatten)] + pub libs: BTreeMap, } #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] @@ -403,11 +468,26 @@ impl CompilerOutput { /// Finds the first contract with the given name pub fn find(&self, contract: impl AsRef) -> Option { - let contract = contract.as_ref(); - self.contracts - .values() - .find_map(|contracts| contracts.get(contract)) - .map(CompactContractRef::from) + let contract_name = contract.as_ref(); + self.contracts_iter().find_map(|(name, contract)| { + (name == contract_name).then(|| CompactContractRef::from(contract)) + }) + } + + /// Finds the first contract with the given name and removes it from the set + pub fn remove(&mut self, contract: impl AsRef) -> Option { + let contract_name = contract.as_ref(); + self.contracts.values_mut().find_map(|c| c.remove(contract_name)) + } + + /// Iterate over all contracts and their names + pub fn contracts_iter(&self) -> impl Iterator { + self.contracts.values().flatten() + } + + /// Iterate over all contracts and their names + pub fn contracts_into_iter(self) -> impl Iterator { + self.contracts.into_values().flatten() } /// Given the contract file's path and the contract's name, tries to return the contract's @@ -454,11 +534,11 @@ impl<'a> fmt::Display for OutputDiagnostics<'a> { #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct Contract { - /// The Ethereum Contract ABI. - /// See https://docs.soliditylang.org/en/develop/abi-spec.html + /// The Ethereum Contract Metadata. + /// See https://docs.soliditylang.org/en/develop/metadata.html pub abi: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub metadata: Option, + #[serde(default, skip_serializing_if = "Option::is_none", with = "json_string_opt")] + pub metadata: Option, #[serde(default)] pub userdoc: UserDoc, #[serde(default)] @@ -699,6 +779,7 @@ pub struct Ewasm { #[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)] pub struct StorageLayout { pub storage: Vec, + #[serde(default)] pub types: BTreeMap, } @@ -885,6 +966,38 @@ mod display_from_str_opt { } } +mod json_string_opt { + use serde::{ + de::{self, DeserializeOwned}, + ser, Deserialize, Deserializer, Serialize, Serializer, + }; + + pub fn serialize(value: &Option, serializer: S) -> Result + where + S: Serializer, + T: Serialize, + { + if let Some(value) = value { + let value = serde_json::to_string(value).map_err(ser::Error::custom)?; + serializer.serialize_str(&value) + } else { + serializer.serialize_none() + } + } + + pub fn deserialize<'de, T, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + T: DeserializeOwned, + { + if let Some(s) = Option::::deserialize(deserializer)? { + serde_json::from_str(&s).map_err(de::Error::custom).map(Some) + } else { + Ok(None) + } + } +} + pub fn deserialize_bytes<'de, D>(d: D) -> std::result::Result where D: Deserializer<'de>, @@ -899,7 +1012,15 @@ where { let value = Option::::deserialize(d)?; if let Some(value) = value { - Ok(Some(hex::decode(&value).map_err(|e| serde::de::Error::custom(e.to_string()))?.into())) + Ok(Some( + if let Some(value) = value.strip_prefix("0x") { + hex::decode(value) + } else { + hex::decode(&value) + } + .map_err(|e| serde::de::Error::custom(e.to_string()))? + .into(), + )) } else { Ok(None) } diff --git a/ethers-solc/src/cache.rs b/ethers-solc/src/cache.rs index ae12591f..626cc4d4 100644 --- a/ethers-solc/src/cache.rs +++ b/ethers-solc/src/cache.rs @@ -3,7 +3,7 @@ use crate::{ artifacts::Sources, config::SolcConfig, error::{Result, SolcError}, - utils, + utils, ArtifactOutput, }; use serde::{Deserialize, Serialize}; use std::{ @@ -93,6 +93,75 @@ impl SolFilesCache { true } } + + /// Returns only the files that were changed or are missing artifacts compared to previous + /// compiler execution, to save time when compiling. + pub fn get_changed_or_missing_artifacts_files<'a, T: ArtifactOutput>( + &'a self, + sources: Sources, + config: Option<&'a SolcConfig>, + artifacts_root: &Path, + ) -> Sources { + sources + .into_iter() + .filter(move |(file, source)| { + self.has_changed_or_missing_artifact::( + file, + source.content_hash().as_bytes(), + config, + artifacts_root, + ) + }) + .collect() + } + + /// Returns true if the given content hash or config differs from the file's + /// or the file does not exist or the files' artifacts are missing + pub fn has_changed_or_missing_artifact( + &self, + file: &Path, + hash: &[u8], + config: Option<&SolcConfig>, + artifacts_root: &Path, + ) -> bool { + if let Some(entry) = self.files.get(file) { + if entry.content_hash.as_bytes() != hash { + return true + } + if let Some(config) = config { + if config != &entry.solc_config { + return true + } + } + + entry.artifacts.iter().any(|name| !T::output_exists(file, name, artifacts_root)) + } else { + true + } + } + + /// Checks if all artifact files exist + pub fn all_artifacts_exist(&self, artifacts_root: &Path) -> bool { + self.files.iter().all(|(file, entry)| { + entry.artifacts.iter().all(|name| T::output_exists(file, name, artifacts_root)) + }) + } + + /// Reads all cached artifacts from disk + pub fn read_artifacts( + &self, + artifacts_root: &Path, + ) -> Result> { + let mut artifacts = BTreeMap::default(); + for (file, entry) in &self.files { + for artifact in &entry.artifacts { + let artifact_file = artifacts_root.join(T::output_file(file, artifact)); + let artifact = T::read_cached_artifact(&artifact_file)?; + artifacts.insert(artifact_file, artifact); + } + } + Ok(artifacts) + } } #[cfg(feature = "async")] diff --git a/ethers-solc/src/config.rs b/ethers-solc/src/config.rs index 0e4af81d..d42e9c33 100644 --- a/ethers-solc/src/config.rs +++ b/ethers-solc/src/config.rs @@ -1,12 +1,15 @@ use crate::{ - artifacts::{CompactContractRef, Settings}, + artifacts::{CompactContract, CompactContractRef, Contract, Settings}, cache::SOLIDITY_FILES_CACHE_FILENAME, error::Result, remappings::Remapping, CompilerOutput, Solc, }; +use ethers_core::{abi::Abi, types::Bytes}; use serde::{Deserialize, Serialize}; use std::{ + collections::BTreeMap, + convert::TryFrom, fmt, fs, io, path::{Path, PathBuf}, }; @@ -224,76 +227,172 @@ impl SolcConfigBuilder { } } -/// Determines how to handle compiler output -pub enum ArtifactOutput { - /// No-op, does not write the artifacts to disk. - Nothing, - /// Creates a single json artifact with - /// ```json - /// { - /// "abi": [], - /// "bin": "...", - /// "runtime-bin": "..." - /// } - /// ``` - MinimalCombined, - /// Hardhat style artifacts - Hardhat, - /// Custom output handler - Custom(Box Result<()>>), +pub type Artifacts = BTreeMap>; + +pub trait Artifact { + fn into_inner(self) -> (Option, Option); } -impl ArtifactOutput { - /// Is expected to handle the output and where to store it - pub fn on_output(&self, output: &CompilerOutput, layout: &ProjectPathsConfig) -> Result<()> { - match self { - ArtifactOutput::Nothing => Ok(()), - ArtifactOutput::MinimalCombined => { - fs::create_dir_all(&layout.artifacts)?; +impl Artifact for CompactContract { + fn into_inner(self) -> (Option, Option) { + (self.abi, self.bin) + } +} - for contracts in output.contracts.values() { - for (name, contract) in contracts { - let file = layout.artifacts.join(format!("{}.json", name)); - let min = CompactContractRef::from(contract); - fs::write(file, serde_json::to_vec_pretty(&min)?)? - } +impl Artifact for serde_json::Value { + fn into_inner(self) -> (Option, Option) { + let abi = self.get("abi").map(|abi| { + serde_json::from_value::(abi.clone()).expect("could not get artifact abi") + }); + let bytecode = self.get("bin").map(|bin| { + serde_json::from_value::(bin.clone()).expect("could not get artifact bytecode") + }); + + (abi, bytecode) + } +} + +pub trait ArtifactOutput { + /// How Artifacts are stored + type Artifact: Artifact; + + /// Handle the compiler output. + fn on_output(output: &CompilerOutput, layout: &ProjectPathsConfig) -> Result<()>; + + /// Returns the file name for the contract's artifact + fn output_file_name(name: impl AsRef) -> PathBuf { + format!("{}.json", name.as_ref()).into() + } + + /// Returns the path to the contract's artifact location based on the contract's file and name + /// + /// This returns `contract.sol/contract.json` by default + fn output_file(contract_file: impl AsRef, name: impl AsRef) -> PathBuf { + let name = name.as_ref(); + contract_file + .as_ref() + .file_name() + .map(Path::new) + .map(|p| p.join(Self::output_file_name(name))) + .unwrap_or_else(|| Self::output_file_name(name)) + } + + /// The inverse of `contract_file_name` + /// + /// Expected to return the solidity contract's name derived from the file path + /// `sources/Greeter.sol` -> `Greeter` + fn contract_name(file: impl AsRef) -> Option { + file.as_ref().file_stem().and_then(|s| s.to_str().map(|s| s.to_string())) + } + + /// Whether the corresponding artifact of the given contract file and name exists + fn output_exists( + contract_file: impl AsRef, + name: impl AsRef, + root: impl AsRef, + ) -> bool { + root.as_ref().join(Self::output_file(contract_file, name)).exists() + } + + fn read_cached_artifact(path: impl AsRef) -> Result; + + /// Read the cached artifacts from disk + fn read_cached_artifacts(files: I) -> Result> + where + I: IntoIterator, + T: Into, + { + let mut artifacts = BTreeMap::default(); + for path in files.into_iter() { + let path = path.into(); + let artifact = Self::read_cached_artifact(&path)?; + artifacts.insert(path, artifact); + } + Ok(artifacts) + } + + /// Convert a contract to the artifact type + fn contract_to_artifact(contract: Contract) -> Self::Artifact; + + /// Convert the compiler output into a set of artifacts + fn output_to_artifacts(output: CompilerOutput) -> Artifacts { + output + .contracts + .into_iter() + .map(|(s, contracts)| { + ( + s, + contracts + .into_iter() + .map(|(s, c)| (s, Self::contract_to_artifact(c))) + .collect(), + ) + }) + .collect() + } +} + +/// An Artifacts implementation that uses a compact representation +/// +/// Creates a single json artifact with +/// ```json +/// { +/// "abi": [], +/// "bin": "...", +/// "runtime-bin": "..." +/// } +/// ``` +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct MinimalCombinedArtifacts; + +impl ArtifactOutput for MinimalCombinedArtifacts { + type Artifact = CompactContract; + + fn on_output(output: &CompilerOutput, layout: &ProjectPathsConfig) -> Result<()> { + fs::create_dir_all(&layout.artifacts)?; + for (file, contracts) in output.contracts.iter() { + for (name, contract) in contracts { + let artifact = Self::output_file(file, name); + let file = layout.artifacts.join(artifact); + if let Some(parent) = file.parent() { + fs::create_dir_all(parent)?; } - Ok(()) - } - ArtifactOutput::Hardhat => { - todo!("Hardhat style artifacts not yet implemented") - } - ArtifactOutput::Custom(f) => f(output, layout), - } - } -} - -impl Default for ArtifactOutput { - fn default() -> Self { - ArtifactOutput::MinimalCombined - } -} - -impl fmt::Debug for ArtifactOutput { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - ArtifactOutput::Nothing => { - write!(f, "Nothing") - } - ArtifactOutput::MinimalCombined => { - write!(f, "MinimalCombined") - } - ArtifactOutput::Hardhat => { - write!(f, "Hardhat") - } - ArtifactOutput::Custom(_) => { - write!(f, "Custom") + let min = CompactContractRef::from(contract); + fs::write(file, serde_json::to_vec_pretty(&min)?)? } } + Ok(()) + } + + fn read_cached_artifact(path: impl AsRef) -> Result { + let file = fs::File::open(path.as_ref())?; + Ok(serde_json::from_reader(file)?) + } + + fn contract_to_artifact(contract: Contract) -> Self::Artifact { + CompactContract::from(contract) } } -use std::convert::TryFrom; +/// Hardhat style artifacts +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct HardhatArtifacts; + +impl ArtifactOutput for HardhatArtifacts { + type Artifact = serde_json::Value; + + fn on_output(_output: &CompilerOutput, _layout: &ProjectPathsConfig) -> Result<()> { + todo!("Hardhat style artifacts not yet implemented") + } + + fn read_cached_artifact(_path: impl AsRef) -> Result { + todo!("Hardhat style artifacts not yet implemented") + } + + fn contract_to_artifact(_contract: Contract) -> Self::Artifact { + todo!("Hardhat style artifacts not yet implemented") + } +} /// Helper struct for serializing `--allow-paths` arguments to Solc /// diff --git a/ethers-solc/src/lib.rs b/ethers-solc/src/lib.rs index b60b68b0..d6132555 100644 --- a/ethers-solc/src/lib.rs +++ b/ethers-solc/src/lib.rs @@ -11,7 +11,10 @@ mod compile; pub use compile::*; mod config; -pub use config::{AllowedLibPaths, ArtifactOutput, ProjectPathsConfig, SolcConfig}; +pub use config::{ + AllowedLibPaths, Artifact, ArtifactOutput, MinimalCombinedArtifacts, ProjectPathsConfig, + SolcConfig, +}; pub mod remappings; @@ -22,15 +25,17 @@ pub mod utils; use crate::artifacts::Sources; use error::Result; use std::{ + borrow::Cow, collections::{BTreeMap, HashMap}, convert::TryInto, fmt, fs, io, + marker::PhantomData, path::PathBuf, }; /// Handles contract compiling #[derive(Debug)] -pub struct Project { +pub struct Project { /// The layout of the pub paths: ProjectPathsConfig, /// Where to find solc @@ -39,8 +44,10 @@ pub struct Project { pub solc_config: SolcConfig, /// Whether caching is enabled pub cached: bool, + /// Whether writing artifacts to disk is enabled + pub no_artifacts: bool, /// How to handle compiler output - pub artifacts: ArtifactOutput, + pub artifacts: PhantomData, /// Errors/Warnings which match these error codes are not going to be logged pub ignored_error_codes: Vec, /// The paths which will be allowed for library inclusion @@ -48,23 +55,53 @@ pub struct Project { } impl Project { - /// Configure the current project + /// Convenience function to call `ProjectBuilder::default()` /// /// # Example /// + /// Configure with `MinimalCombinedArtifacts` artifacts output + /// /// ```rust /// use ethers_solc::Project; /// let config = Project::builder().build().unwrap(); /// ``` + /// + /// To configure any a project with any `ArtifactOutput` use either + /// + /// ```rust + /// use ethers_solc::Project; + /// let config = Project::builder().build().unwrap(); + /// ``` + /// + /// or use the builder directly + /// + /// ```rust + /// use ethers_solc::{MinimalCombinedArtifacts, ProjectBuilder}; + /// let config = ProjectBuilder::::default().build().unwrap(); + /// ``` pub fn builder() -> ProjectBuilder { ProjectBuilder::default() } +} - fn write_cache_file(&self, sources: Sources) -> Result<()> { - let cache = SolFilesCache::builder() +impl Project { + fn write_cache_file( + &self, + sources: Sources, + artifacts: Vec<(PathBuf, Vec)>, + ) -> Result<()> { + let mut cache = SolFilesCache::builder() .root(&self.paths.root) .solc_config(self.solc_config.clone()) .insert_files(sources)?; + + // add the artifacts for each file to the cache entry + for (file, artifacts) in artifacts { + if let Some(entry) = cache.files.get_mut(&file) { + entry.artifacts = artifacts; + } + } + if let Some(cache_dir) = self.paths.cache.parent() { fs::create_dir_all(cache_dir)? } @@ -101,7 +138,7 @@ impl Project { /// NB: If the `svm` feature is enabled, this function will automatically detect /// solc versions across files. - pub fn compile(&self) -> Result { + pub fn compile(&self) -> Result> { let sources = self.sources()?; #[cfg(not(all(feature = "svm", feature = "async")))] @@ -113,7 +150,7 @@ impl Project { } #[cfg(all(feature = "svm", feature = "async"))] - fn svm_compile(&self, sources: Sources) -> Result { + fn svm_compile(&self, sources: Sources) -> Result> { // split them by version let mut sources_by_version = BTreeMap::new(); for (path, source) in sources.into_iter() { @@ -131,44 +168,57 @@ impl Project { entry.insert(path, source); } + let mut compiled = + ProjectCompileOutput::with_ignored_errors(self.ignored_error_codes.clone()); + // run the compilation step for each version - let mut res = CompilerOutput::default(); for (solc, sources) in sources_by_version { - let output = self.compile_with_version(&solc, sources)?; - if let ProjectCompileOutput::Compiled((compiled, _)) = output { - res.errors.extend(compiled.errors); - res.sources.extend(compiled.sources); - res.contracts.extend(compiled.contracts); - } + compiled.extend(self.compile_with_version(&solc, sources)?); } - Ok(if res.contracts.is_empty() && res.errors.is_empty() { - ProjectCompileOutput::Unchanged - } else { - ProjectCompileOutput::Compiled((res, &self.ignored_error_codes)) - }) + if !compiled.has_compiled_contracts() && + !compiled.has_compiler_errors() && + self.cached && + self.paths.cache.exists() + { + let cache = SolFilesCache::read(&self.paths.cache)?; + let artifacts = cache.read_artifacts::(&self.paths.artifacts)?; + compiled.artifacts.extend(artifacts); + } + Ok(compiled) } pub fn compile_with_version( &self, solc: &Solc, mut sources: Sources, - ) -> Result { + ) -> Result> { // add all libraries to the source set while keeping track of their actual disk path - let mut source_name_path = HashMap::new(); - let mut path_source_name = HashMap::new(); + // (`contracts/contract.sol` -> `/Users/.../contracts.sol`) + let mut source_name_to_path = HashMap::new(); + // inverse of `source_name_to_path` : (`/Users/.../contracts.sol` -> + // `contracts/contract.sol`) + let mut path_to_source_name = HashMap::new(); + for (import, (source, path)) in self.resolved_libraries(&sources)? { // inserting with absolute path here and keep track of the source name <-> path mappings sources.insert(path.clone(), source); - path_source_name.insert(path.clone(), import.clone()); - source_name_path.insert(import, path); + path_to_source_name.insert(path.clone(), import.clone()); + source_name_to_path.insert(import, path); } // If there's a cache set, filter to only re-compile the files which were changed let sources = if self.cached && self.paths.cache.exists() { let cache = SolFilesCache::read(&self.paths.cache)?; - let changed_files = cache.get_changed_files(sources, Some(&self.solc_config)); + let changed_files = cache.get_changed_or_missing_artifacts_files::( + sources, + Some(&self.solc_config), + &self.paths.artifacts, + ); + + // if nothing changed and all artifacts still exist if changed_files.is_empty() { - return Ok(ProjectCompileOutput::Unchanged) + let artifacts = cache.read_artifacts::(&self.paths.artifacts)?; + return Ok(ProjectCompileOutput::from_unchanged(artifacts)) } changed_files } else { @@ -176,25 +226,43 @@ impl Project { }; // replace absolute path with source name to make solc happy - let sources = apply_mappings(sources, path_source_name); + let sources = apply_mappings(sources, path_to_source_name); let input = CompilerInput::with_sources(sources) .normalize_evm_version(&solc.version()?) .with_remappings(self.paths.remappings.clone()); let output = solc.compile(&input)?; if output.has_error() { - return Ok(ProjectCompileOutput::Compiled((output, &self.ignored_error_codes))) + return Ok(ProjectCompileOutput::from_compiler_output( + output, + self.ignored_error_codes.clone(), + )) } if self.cached { + // get all contract names of the files and map them to the disk file + let artifacts = output + .contracts + .iter() + .map(|(path, contracts)| { + let path = PathBuf::from(path); + let file = source_name_to_path.get(&path).cloned().unwrap_or(path); + (file, contracts.keys().cloned().collect::>()) + }) + .collect::>(); + // reapply to disk paths - let sources = apply_mappings(input.sources, source_name_path); + let sources = apply_mappings(input.sources, source_name_to_path); + // create cache file - self.write_cache_file(sources)?; + self.write_cache_file(sources, artifacts)?; } - self.artifacts.on_output(&output, &self.paths)?; - Ok(ProjectCompileOutput::Compiled((output, &self.ignored_error_codes))) + // TODO: There seems to be some type redundancy here, c.f. discussion with @mattsse + if !self.no_artifacts { + Artifacts::on_output(&output, &self.paths)?; + } + Ok(ProjectCompileOutput::from_compiler_output(output, self.ignored_error_codes.clone())) } } @@ -211,7 +279,7 @@ fn apply_mappings(sources: Sources, mut mappings: HashMap) -> .collect() } -pub struct ProjectBuilder { +pub struct ProjectBuilder { /// The layout of the paths: Option, /// Where to find solc @@ -220,15 +288,16 @@ pub struct ProjectBuilder { solc_config: Option, /// Whether caching is enabled, default is true. cached: bool, - /// How to handle compiler output - artifacts: Option, + /// Whether writing artifacts to disk is enabled, default is true. + no_artifacts: bool, + artifacts: PhantomData, /// Which error codes to ignore pub ignored_error_codes: Vec, /// All allowed paths pub allowed_paths: Vec, } -impl ProjectBuilder { +impl ProjectBuilder { pub fn paths(mut self, paths: ProjectPathsConfig) -> Self { self.paths = Some(paths); self @@ -244,11 +313,6 @@ impl ProjectBuilder { self } - pub fn artifacts(mut self, artifacts: ArtifactOutput) -> Self { - self.artifacts = Some(artifacts); - self - } - pub fn ignore_error_code(mut self, code: u64) -> Self { self.ignored_error_codes.push(code); self @@ -260,6 +324,36 @@ impl ProjectBuilder { self } + /// Disables writing artifacts to disk + pub fn no_artifacts(mut self) -> Self { + self.no_artifacts = true; + self + } + + /// Set arbitrary `ArtifactOutputHandler` + pub fn artifacts(self) -> ProjectBuilder { + let ProjectBuilder { + paths, + solc, + solc_config, + cached, + no_artifacts, + ignored_error_codes, + allowed_paths, + .. + } = self; + ProjectBuilder { + paths, + solc, + solc_config, + cached, + no_artifacts, + artifacts: PhantomData::default(), + ignored_error_codes, + allowed_paths, + } + } + /// Adds an allowed-path to the solc executable pub fn allowed_path>(mut self, path: T) -> Self { self.allowed_paths.push(path.into()); @@ -278,12 +372,13 @@ impl ProjectBuilder { self } - pub fn build(self) -> Result { + pub fn build(self) -> Result> { let Self { paths, solc, solc_config, cached, + no_artifacts, artifacts, ignored_error_codes, mut allowed_paths, @@ -307,43 +402,197 @@ impl ProjectBuilder { solc, solc_config, cached, - artifacts: artifacts.unwrap_or_default(), + no_artifacts, + artifacts, ignored_error_codes, allowed_lib_paths: allowed_paths.try_into()?, }) } } -impl Default for ProjectBuilder { +impl Default for ProjectBuilder { fn default() -> Self { Self { paths: None, solc: None, solc_config: None, cached: true, - artifacts: None, + no_artifacts: false, + artifacts: PhantomData::default(), ignored_error_codes: Vec::new(), allowed_paths: vec![], } } } -#[derive(Debug, Clone, PartialEq)] -pub enum ProjectCompileOutput<'a> { - /// Nothing to compile because unchanged sources - Unchanged, - Compiled((CompilerOutput, &'a [u64])), +/// The outcome of `Project::compile` +#[derive(Debug, Clone, PartialEq, Default)] +pub struct ProjectCompileOutput { + /// If solc was invoked multiple times in `Project::compile` then this contains a merged + /// version of all `CompilerOutput`s. If solc was called only once then `compiler_output` + /// holds the `CompilerOutput` of that call. + compiler_output: Option, + /// All artifacts that were read from cache + artifacts: BTreeMap, + ignored_error_codes: Vec, } -impl<'a> fmt::Display for ProjectCompileOutput<'a> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - ProjectCompileOutput::Unchanged => f.write_str("Nothing to compile"), - ProjectCompileOutput::Compiled((output, ignored_error_codes)) => { - output.diagnostics(ignored_error_codes).fmt(f) +impl ProjectCompileOutput { + pub fn with_ignored_errors(ignored_errors: Vec) -> Self { + Self { + compiler_output: None, + artifacts: Default::default(), + ignored_error_codes: ignored_errors, + } + } + + pub fn from_unchanged(artifacts: BTreeMap) -> Self { + Self { compiler_output: None, artifacts, ignored_error_codes: vec![] } + } + + pub fn from_compiler_output( + compiler_output: CompilerOutput, + ignored_error_codes: Vec, + ) -> Self { + Self { + compiler_output: Some(compiler_output), + artifacts: Default::default(), + ignored_error_codes, + } + } + + /// Get the (merged) solc compiler output + /// ```no_run + /// use std::collections::BTreeMap; + /// use ethers_solc::artifacts::Contract; + /// use ethers_solc::Project; + /// + /// let project = Project::builder().build().unwrap(); + /// let contracts: BTreeMap = + /// project.compile().unwrap().output().contracts_into_iter().collect(); + /// ``` + pub fn output(self) -> CompilerOutput { + self.compiler_output.unwrap_or_default() + } + + /// Combine two outputs + pub fn extend(&mut self, compiled: ProjectCompileOutput) { + let ProjectCompileOutput { compiler_output, artifacts, .. } = compiled; + self.artifacts.extend(artifacts); + if let Some(compiled) = compiler_output { + if let Some(output) = self.compiler_output.as_mut() { + output.errors.extend(compiled.errors); + output.sources.extend(compiled.sources); + output.contracts.extend(compiled.contracts); + } else { + self.compiler_output = Some(compiled); } } } + + /// Whether this type does not contain compiled contracts + pub fn is_unchanged(&self) -> bool { + !self.has_compiled_contracts() + } + + /// Whether this type has a compiler output + pub fn has_compiled_contracts(&self) -> bool { + if let Some(output) = self.compiler_output.as_ref() { + !output.contracts.is_empty() + } else { + false + } + } + + /// Whether there were errors + pub fn has_compiler_errors(&self) -> bool { + if let Some(output) = self.compiler_output.as_ref() { + output.has_error() + } else { + false + } + } + + /// Finds the first contract with the given name and removes it from the set + pub fn remove(&mut self, contract: impl AsRef) -> Option { + let contract = contract.as_ref(); + if let Some(output) = self.compiler_output.as_mut() { + if let contract @ Some(_) = output + .contracts + .values_mut() + .find_map(|c| c.remove(contract).map(T::contract_to_artifact)) + { + return contract + } + } + let key = self + .artifacts + .iter() + .find_map(|(path, _)| { + T::contract_name(path).filter(|name| name == contract).map(|_| path) + })? + .clone(); + self.artifacts.remove(&key) + } +} + +impl ProjectCompileOutput +where + T::Artifact: Clone, +{ + /// Finds the first contract with the given name + pub fn find(&self, contract: impl AsRef) -> Option> { + let contract = contract.as_ref(); + if let Some(output) = self.compiler_output.as_ref() { + if let contract @ Some(_) = output.contracts.values().find_map(|c| { + c.get(contract).map(|c| T::contract_to_artifact(c.clone())).map(Cow::Owned) + }) { + return contract + } + } + self.artifacts.iter().find_map(|(path, art)| { + T::contract_name(path).filter(|name| name == contract).map(|_| Cow::Borrowed(art)) + }) + } +} + +impl ProjectCompileOutput { + /// All artifacts together with their contract name + /// + /// # Example + /// + /// ```no_run + /// use std::collections::BTreeMap; + /// use ethers_solc::artifacts::CompactContract; + /// use ethers_solc::Project; + /// + /// let project = Project::builder().build().unwrap(); + /// let contracts: BTreeMap = project.compile().unwrap().into_artifacts().collect(); + /// ``` + pub fn into_artifacts(mut self) -> Box> { + let artifacts = self.artifacts.into_iter().filter_map(|(path, art)| { + T::contract_name(&path) + .map(|name| (format!("{:?}:{}", path.file_name().unwrap(), name), art)) + }); + + let artifacts: Box> = + if let Some(output) = self.compiler_output.take() { + Box::new(artifacts.chain(T::output_to_artifacts(output).into_values().flatten())) + } else { + Box::new(artifacts) + }; + artifacts + } +} + +impl fmt::Display for ProjectCompileOutput { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(output) = self.compiler_output.as_ref() { + output.diagnostics(&self.ignored_error_codes).fmt(f) + } else { + f.write_str("Nothing to compile") + } + } } #[cfg(test)] @@ -359,20 +608,10 @@ mod tests { .sources("./test-data/test-contract-versions") .build() .unwrap(); - let project = Project::builder() - .paths(paths) - .ephemeral() - .artifacts(ArtifactOutput::Nothing) - .build() - .unwrap(); + let project = Project::builder().paths(paths).no_artifacts().ephemeral().build().unwrap(); let compiled = project.compile().unwrap(); - let contracts = match compiled { - ProjectCompileOutput::Compiled((out, _)) => { - assert!(!out.has_error()); - out.contracts - } - _ => panic!("must compile"), - }; + assert!(!compiled.has_compiler_errors()); + let contracts = compiled.output().contracts; // Contracts A to F assert_eq!(contracts.keys().count(), 5); } @@ -393,18 +632,14 @@ mod tests { .unwrap(); let project = Project::builder() .paths(paths) + .no_artifacts() .ephemeral() - .artifacts(ArtifactOutput::Nothing) + .no_artifacts() .build() .unwrap(); let compiled = project.compile().unwrap(); - let contracts = match compiled { - ProjectCompileOutput::Compiled((out, _)) => { - assert!(!out.has_error()); - out.contracts - } - _ => panic!("must compile"), - }; + assert!(!compiled.has_compiler_errors()); + let contracts = compiled.output().contracts; assert_eq!(contracts.keys().count(), 3); } @@ -420,20 +655,10 @@ mod tests { .lib(root.join("lib")) .build() .unwrap(); - let project = Project::builder() - .paths(paths) - .ephemeral() - .artifacts(ArtifactOutput::Nothing) - .build() - .unwrap(); + let project = Project::builder().no_artifacts().paths(paths).ephemeral().build().unwrap(); let compiled = project.compile().unwrap(); - let contracts = match compiled { - ProjectCompileOutput::Compiled((out, _)) => { - assert!(!out.has_error()); - out.contracts - } - _ => panic!("must compile"), - }; + assert!(!compiled.has_compiler_errors()); + let contracts = compiled.output().contracts; assert_eq!(contracts.keys().count(), 2); } } diff --git a/ethers-solc/src/remappings.rs b/ethers-solc/src/remappings.rs index c55abb0b..d5a4bd13 100644 --- a/ethers-solc/src/remappings.rs +++ b/ethers-solc/src/remappings.rs @@ -182,7 +182,7 @@ mod tests { #[test] fn serde() { let remapping = "oz=../b/c/d"; - let remapping = Remapping::from_str(&remapping).unwrap(); + let remapping = Remapping::from_str(remapping).unwrap(); assert_eq!(remapping.name, "oz".to_string()); assert_eq!(remapping.path, "../b/c/d".to_string()); diff --git a/ethers-solc/test-data/dapp-sample/src/Dapp.sol b/ethers-solc/test-data/dapp-sample/src/Dapp.sol index 17cb31cb..906f3070 100644 --- a/ethers-solc/test-data/dapp-sample/src/Dapp.sol +++ b/ethers-solc/test-data/dapp-sample/src/Dapp.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.6.6; +pragma solidity >=0.6.6; contract Dapp { } diff --git a/ethers-solc/test-data/dapp-sample/src/Dapp.t.sol b/ethers-solc/test-data/dapp-sample/src/Dapp.t.sol index b9d67b04..e0f78a35 100644 --- a/ethers-solc/test-data/dapp-sample/src/Dapp.t.sol +++ b/ethers-solc/test-data/dapp-sample/src/Dapp.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.6.6; +pragma solidity >=0.6.6; import "ds-test/test.sol"; diff --git a/ethers-solc/test-data/hardhat-sample/contracts/Greeter.sol b/ethers-solc/test-data/hardhat-sample/contracts/Greeter.sol index 7e709f22..b7e54a6f 100644 --- a/ethers-solc/test-data/hardhat-sample/contracts/Greeter.sol +++ b/ethers-solc/test-data/hardhat-sample/contracts/Greeter.sol @@ -1,5 +1,5 @@ //SPDX-License-Identifier: Unlicense -pragma solidity ^0.6.0; +pragma solidity >=0.6.0; import "hardhat/console.sol"; diff --git a/ethers-solc/test-data/out/compiler-out-1.json b/ethers-solc/test-data/out/compiler-out-1.json index 0dfd298e..2c1043f0 100644 --- a/ethers-solc/test-data/out/compiler-out-1.json +++ b/ethers-solc/test-data/out/compiler-out-1.json @@ -33,7 +33,6 @@ "sourceFile.sol": { "ContractName": { "abi": [], - "metadata": "{/* ... */}", "userdoc": {}, "devdoc": {}, "ir": "", diff --git a/ethers-solc/tests/project.rs b/ethers-solc/tests/project.rs index c6cd0367..b5d90b7b 100644 --- a/ethers-solc/tests/project.rs +++ b/ethers-solc/tests/project.rs @@ -1,8 +1,6 @@ //! project tests -use ethers_solc::{ - cache::SOLIDITY_FILES_CACHE_FILENAME, Project, ProjectCompileOutput, ProjectPathsConfig, -}; +use ethers_solc::{cache::SOLIDITY_FILES_CACHE_FILENAME, Project, ProjectPathsConfig}; use std::path::PathBuf; use tempdir::TempDir; @@ -26,12 +24,22 @@ fn can_compile_hardhat_sample() { let project = Project::builder().paths(paths).build().unwrap(); let compiled = project.compile().unwrap(); - match compiled { - ProjectCompileOutput::Compiled((out, _)) => assert!(!out.has_error()), - _ => panic!("must compile"), - } + assert!(compiled.find("Greeter").is_some()); + assert!(compiled.find("console").is_some()); + assert!(!compiled.has_compiler_errors()); + // nothing to compile - assert_eq!(project.compile().unwrap(), ProjectCompileOutput::Unchanged); + let compiled = project.compile().unwrap(); + assert!(compiled.find("Greeter").is_some()); + assert!(compiled.find("console").is_some()); + assert!(compiled.is_unchanged()); + + // delete artifacts + std::fs::remove_dir_all(&project.paths.artifacts).unwrap(); + let compiled = project.compile().unwrap(); + assert!(compiled.find("Greeter").is_some()); + assert!(compiled.find("console").is_some()); + assert!(!compiled.is_unchanged()); } #[test] @@ -53,10 +61,17 @@ fn can_compile_dapp_sample() { let project = Project::builder().paths(paths).build().unwrap(); let compiled = project.compile().unwrap(); - match compiled { - ProjectCompileOutput::Compiled((out, _)) => assert!(!out.has_error()), - _ => panic!("must compile"), - } + assert!(compiled.find("Dapp").is_some()); + assert!(!compiled.has_compiler_errors()); + // nothing to compile - assert_eq!(project.compile().unwrap(), ProjectCompileOutput::Unchanged); + let compiled = project.compile().unwrap(); + assert!(compiled.find("Dapp").is_some()); + assert!(compiled.is_unchanged()); + + // delete artifacts + std::fs::remove_dir_all(&project.paths.artifacts).unwrap(); + let compiled = project.compile().unwrap(); + assert!(compiled.find("Dapp").is_some()); + assert!(!compiled.is_unchanged()); }