From 0464ac9d46d32b08c05ebea1fcd82d8a04ca61d4 Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Sat, 4 Dec 2021 18:13:58 +0100 Subject: [PATCH] feat(solc): improve solc detection and reduce install effort (#648) --- ethers-solc/src/compile.rs | 23 ++++++++++++++++------- ethers-solc/src/error.rs | 10 +++++----- ethers-solc/src/lib.rs | 28 +++++++++++++++++++++------- ethers-solc/src/utils.rs | 21 +++++++++++++++++++++ 4 files changed, 63 insertions(+), 19 deletions(-) diff --git a/ethers-solc/src/compile.rs b/ethers-solc/src/compile.rs index 5b652acd..c7ebcbc7 100644 --- a/ethers-solc/src/compile.rs +++ b/ethers-solc/src/compile.rs @@ -1,7 +1,7 @@ use crate::{ artifacts::Source, error::{Result, SolcError}, - CompilerInput, CompilerOutput, + utils, CompilerInput, CompilerOutput, }; use semver::{Version, VersionReq}; use serde::{de::DeserializeOwned, Serialize}; @@ -157,17 +157,25 @@ impl Solc { pub fn detect_version(source: &Source) -> Result { // detects the required solc version let sol_version = Self::version_req(source)?; + Self::ensure_installed(&sol_version) + } + /// Given a Solidity version requirement, it detects the latest compiler version which can be + /// used to build it, and returns it. + /// + /// If the required compiler version is not installed, it also proceeds to install it. + #[cfg(all(feature = "svm", feature = "async"))] + pub fn ensure_installed(sol_version: &VersionReq) -> Result { #[cfg(any(test, feature = "tests"))] // take the lock in tests, we use this to enforce that // a test does not run while a compiler version is being installed let _lock = LOCK.lock(); // load the local / remote versions - let versions = svm::installed_versions().unwrap_or_default(); - let local_versions = Self::find_matching_installation(&versions, &sol_version); - let remote_versions = Self::find_matching_installation(&RELEASES.1, &sol_version); + let versions = utils::installed_versions(svm::SVM_HOME.as_path()).unwrap_or_default(); + let local_versions = Self::find_matching_installation(&versions, sol_version); + let remote_versions = Self::find_matching_installation(&RELEASES.1, sol_version); // if there's a better upstream version than the one we have, install it Ok(match (local_versions, remote_versions) { (Some(local), None) => local, @@ -191,7 +199,7 @@ impl Solc { /// Parses the given source looking for the `pragma` definition and /// returns the corresponding SemVer version requirement. pub fn version_req(source: &Source) -> Result { - let version = crate::utils::find_version_pragma(&source.content) + let version = utils::find_version_pragma(&source.content) .ok_or(SolcError::PragmaNotFound)? .replace(" ", ","); @@ -219,12 +227,14 @@ impl Solc { /// ``` #[cfg(feature = "svm")] pub async fn install(version: &Version) -> std::result::Result<(), svm::SolcVmError> { + tracing::trace!("installing solc version \"{}\"", version); svm::install(version).await } /// Blocking version of `Self::install` #[cfg(all(feature = "svm", feature = "async"))] pub fn blocking_install(version: &Version) -> std::result::Result<(), svm::SolcVmError> { + tracing::trace!("blocking installing solc version \"{}\"", version); tokio::runtime::Runtime::new().unwrap().block_on(svm::install(version))?; Ok(()) } @@ -489,7 +499,6 @@ mod tests { ] .iter() { - // println!("Checking {}", pragma); let source = source(pragma); let res = Solc::detect_version(&source).unwrap(); assert_eq!(res, Version::from_str(expected).unwrap()); @@ -504,7 +513,7 @@ mod tests { let _lock = LOCK.lock(); let ver = "0.8.6"; let version = Version::from_str(ver).unwrap(); - if svm::installed_versions() + if utils::installed_versions(svm::SVM_HOME.as_path()) .map(|versions| !versions.contains(&version)) .unwrap_or_default() { diff --git a/ethers-solc/src/error.rs b/ethers-solc/src/error.rs index 86094921..287c10a7 100644 --- a/ethers-solc/src/error.rs +++ b/ethers-solc/src/error.rs @@ -8,24 +8,24 @@ pub enum SolcError { /// Internal solc error #[error("Solc Error: {0}")] SolcError(String), - #[error("missing pragma from solidity file")] + #[error("Missing pragma from solidity file")] PragmaNotFound, - #[error("could not find solc version locally or upstream")] + #[error("Could not find solc version locally or upstream")] VersionNotFound, - #[error("checksum mismatch")] + #[error("Checksum mismatch")] ChecksumMismatch, #[error(transparent)] SemverError(#[from] semver::Error), /// Deserialization error #[error(transparent)] SerdeJson(#[from] serde_json::Error), - /// Deserialization error + /// Filesystem IO error #[error(transparent)] Io(#[from] std::io::Error), #[cfg(feature = "svm")] #[error(transparent)] SvmError(#[from] svm::SolcVmError), - #[error("no contracts found under {0}")] + #[error("No contracts found at \"{0}\"")] NoContracts(String), #[error(transparent)] PatternError(#[from] glob::PatternError), diff --git a/ethers-solc/src/lib.rs b/ethers-solc/src/lib.rs index 77bcb895..a1e2565d 100644 --- a/ethers-solc/src/lib.rs +++ b/ethers-solc/src/lib.rs @@ -3,7 +3,7 @@ pub mod artifacts; pub use artifacts::{CompilerInput, CompilerOutput, EvmVersion}; -use std::collections::btree_map::Entry; +use std::collections::{btree_map::Entry, hash_map}; pub mod cache; @@ -195,18 +195,32 @@ impl Project { #[cfg(all(feature = "svm", feature = "async"))] #[tracing::instrument(skip(self, sources))] fn svm_compile(&self, sources: Sources) -> Result> { + use semver::{Version, VersionReq}; + // split them by version let mut sources_by_version = BTreeMap::new(); // we store the solc versions by path, in case there exists a corrupt solc binary let mut solc_versions = HashMap::new(); - // TODO: Rayon + // tracks unique version requirements to minimize install effort + let mut solc_version_req = HashMap::::new(); + // tracing::trace!("parsing sources"); for (path, source) in sources.into_iter() { - // will detect and install the solc version - // tracing::trace!("finding version {}", path.display()); - let version = Solc::detect_version(&source)?; - // tracing::trace!("found {}", version); + // will detect and install the solc version if it's missing + tracing::trace!("detecting solc version for \"{}\"", path.display()); + let version_req = Solc::version_req(&source)?; + + let version = match solc_version_req.entry(version_req) { + hash_map::Entry::Occupied(version) => version.get().clone(), + hash_map::Entry::Vacant(entry) => { + let version = Solc::ensure_installed(entry.key())?; + entry.insert(version.clone()); + version + } + }; + + tracing::trace!("found installed solc \"{}\"", version); // gets the solc binary for that version, it is expected tha this will succeed // AND find the solc since it was installed right above let mut solc = Solc::find_svm_installed_version(version.to_string())? @@ -236,7 +250,7 @@ impl Project { if let Err(_e) = solc.verify_checksum() { tracing::trace!("corrupted solc version, redownloading..."); Solc::blocking_install(version)?; - tracing::trace!("done."); + tracing::trace!("reinstalled solc: \"{}\"", version); } // once matched, proceed to compile with it tracing::trace!("compiling_with_version"); diff --git a/ethers-solc/src/utils.rs b/ethers-solc/src/utils.rs index 6737edb7..697e6e1a 100644 --- a/ethers-solc/src/utils.rs +++ b/ethers-solc/src/utils.rs @@ -2,8 +2,10 @@ use std::path::{Component, Path, PathBuf}; +use crate::error::SolcError; use once_cell::sync::Lazy; use regex::Regex; +use semver::Version; use walkdir::WalkDir; /// A regex that matches the import path and identifier of a solidity import @@ -102,6 +104,25 @@ pub fn resolve_library(libs: &[impl AsRef], source: impl AsRef) -> O } } +/// Reads the list of Solc versions that have been installed in the machine. The version list is +/// sorted in ascending order. +/// Checks for installed solc versions under the given path as +/// `/`, (e.g.: `~/.svm/0.8.10`) +/// and returns them sorted in ascending order +pub fn installed_versions(root: impl AsRef) -> Result, SolcError> { + let mut versions: Vec<_> = walkdir::WalkDir::new(root) + .max_depth(1) + .into_iter() + .filter_map(std::result::Result::ok) + .filter(|e| e.file_type().is_dir()) + .filter_map(|e: walkdir::DirEntry| { + e.path().file_name().and_then(|v| Version::parse(v.to_string_lossy().as_ref()).ok()) + }) + .collect(); + versions.sort(); + Ok(versions) +} + #[cfg(test)] mod tests { use super::*;