Repository: gakonst/ethers-rs
Pull request #1260 (2022-05-13 23:06:17)

fix(solc): add `RuntimeOrHandle` & fix solc blocking installation
<!--
Thank you for your Pull Request. Please provide a description above and review
the requirements below.

Bug fixes and new features should include tests.

Contributors guide: https://github.com/gakonst/ethers-rs/blob/master/CONTRIBUTING.md

The contributors guide includes instructions for running rustfmt and building the
documentation.
-->

## Motivation

<!--
Explain the context and why you're making that change. What is the problem
you're trying to solve? In some cases there is not a problem and this can be
thought of as being the motivation for your change.
-->

#1259 

## Solution

<!--
Summarize the solution and provide any necessary context needed to understand
the code change.
-->

## PR Checklist

- [x] Added Tests
- [x] Added Documentation
- [ ] Updated the changelog



Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 8fdbc201..458ac4c7 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1401,6 +1401,7 @@ dependencies = [
 name = "ethers-solc"
 version = "0.3.0"
 dependencies = [
+ "cfg-if 1.0.0",
  "colored",
  "criterion",
  "dunce",
diff --git a/ethers-solc/Cargo.toml b/ethers-solc/Cargo.toml
index a184ea32..c0acee2a 100644
--- a/ethers-solc/Cargo.toml
+++ b/ethers-solc/Cargo.toml
@@ -19,7 +19,7 @@ serde_json = "1.0.68"
 serde = { version = "1.0.130", features = ["derive"] }
 semver = { version = "1.0.9", features = ["serde"] }
 walkdir = "2.3.2"
-tokio = { version = "1.15.0", default-features = false, features = ["process", "io-util", "fs", "time"], optional = true }
+tokio = { version = "1.15.0", default-features = false, features = ["rt"] }
 futures-util = { version = "^0.3", optional = true }
 once_cell = "1.10.0"
 regex = "1.5.5"
@@ -39,6 +39,7 @@ solang-parser = { default-features = false, version = "=0.1.13" }
 rayon = "1.5.2"
 rand = { version = "0.8.5", optional = true }
 path-slash = "0.1.4"
+cfg-if = "1.0.0"
 
 [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
 home = "0.5.3"
@@ -80,7 +81,7 @@ required-features = ["full", "project-util"]
 
 [features]
 default = ["rustls"]
-async = ["tokio", "futures-util"]
+async = ["tokio/process", "tokio/io-util", "tokio/fs", "tokio/time", "futures-util"]
 full = ["async", "svm-solc"]
 svm-solc = ["svm/blocking", "svm-builds", "sha2"]
 # Utilities for creating and testing project workspaces
diff --git a/ethers-solc/src/compile/mod.rs b/ethers-solc/src/compile/mod.rs
index 384f9c75..92e3b9a9 100644
--- a/ethers-solc/src/compile/mod.rs
+++ b/ethers-solc/src/compile/mod.rs
@@ -371,9 +371,21 @@ impl Solc {
     /// Blocking version of `Self::install`
     #[cfg(all(feature = "svm-solc"))]
     pub fn blocking_install(version: &Version) -> std::result::Result<Self, svm::SolcVmError> {
+        use crate::utils::RuntimeOrHandle;
+
         tracing::trace!("blocking installing solc version \"{}\"", version);
         crate::report::solc_installation_start(version);
-        match svm::blocking_install(version) {
+        // the async version `svm::install` is used instead of `svm::blocking_intsall`
+        // because the underlying `reqwest::blocking::Client` does not behave well
+        // in tokio rt. see https://github.com/seanmonstar/reqwest/issues/1017
+        cfg_if::cfg_if! {
+            if #[cfg(target_arch = "wasm32")] {
+                let installation = svm::blocking_install(version);
+            } else {
+                let installation = RuntimeOrHandle::new().block_on(svm::install(version));
+            }
+        };
+        match installation {
             Ok(path) => {
                 crate::report::solc_installation_success(version);
                 Ok(Solc::new(path))
@@ -723,6 +735,7 @@ mod tests {
         let other = solc().async_compile(&serde_json::json!(input)).await.unwrap();
         assert_eq!(out, other);
     }
+
     #[cfg(feature = "async")]
     #[tokio::test]
     async fn async_solc_compile_works2() {
@@ -799,6 +812,15 @@ mod tests {
         assert_eq!(res.solc, expected);
     }
 
+    #[test]
+    #[cfg(feature = "svm-solc")]
+    fn can_install_solc_in_tokio_rt() {
+        let version = Version::from_str("0.8.6").unwrap();
+        let rt = tokio::runtime::Runtime::new().unwrap();
+        let result = rt.block_on(async { Solc::blocking_install(&version) });
+        assert!(result.is_ok());
+    }
+
     #[test]
     fn does_not_find_not_installed_version() {
         let ver = "1.1.1";
diff --git a/ethers-solc/src/utils.rs b/ethers-solc/src/utils.rs
index 825107ea..a5bd3e11 100644
--- a/ethers-solc/src/utils.rs
+++ b/ethers-solc/src/utils.rs
@@ -330,6 +330,40 @@ pub(crate) fn find_fave_or_alt_path(root: impl AsRef<Path>, fave: &str, alt: &st
     p
 }
 
+#[cfg(not(target_arch = "wasm32"))]
+use tokio::runtime::{Handle, Runtime};
+
+#[cfg(not(target_arch = "wasm32"))]
+#[derive(Debug)]
+pub enum RuntimeOrHandle {
+    Runtime(Runtime),
+    Handle(Handle),
+}
+
+#[cfg(not(target_arch = "wasm32"))]
+impl Default for RuntimeOrHandle {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+#[cfg(not(target_arch = "wasm32"))]
+impl RuntimeOrHandle {
+    pub fn new() -> RuntimeOrHandle {
+        match Handle::try_current() {
+            Ok(handle) => RuntimeOrHandle::Handle(handle),
+            Err(_) => RuntimeOrHandle::Runtime(Runtime::new().expect("Failed to start runtime")),
+        }
+    }
+
+    pub fn block_on<F: std::future::Future>(&self, f: F) -> F::Output {
+        match &self {
+            RuntimeOrHandle::Runtime(runtime) => runtime.block_on(f),
+            RuntimeOrHandle::Handle(handle) => tokio::task::block_in_place(|| handle.block_on(f)),
+        }
+    }
+}
+
 /// Creates a new named tempdir
 #[cfg(any(test, feature = "project-util"))]
 pub(crate) fn tempdir(name: &str) -> Result<tempfile::TempDir, SolcIoError> {


Test patch:
diff --git a/ethers-solc/tests/project.rs b/ethers-solc/tests/project.rs
index 5bc568e7..21698fd7 100644
--- a/ethers-solc/tests/project.rs
+++ b/ethers-solc/tests/project.rs
@@ -17,6 +17,7 @@ use ethers_solc::{
     ProjectPathsConfig, Solc, TestFileFilter,
 };
 use pretty_assertions::assert_eq;
+use semver::Version;
 
 #[allow(unused)]
 fn init_tracing() {
@@ -1329,3 +1330,53 @@ fn can_compile_model_checker_sample() {
     assert!(!compiled.has_compiler_errors());
     assert!(compiled.has_compiler_warnings());
 }
+
+fn remove_solc_if_exists(version: &Version) {
+    match Solc::find_svm_installed_version(version.to_string()).unwrap() {
+        Some(_) => svm::remove_version(&version).expect("failed to remove version"),
+        None => {}
+    };
+}
+
+#[tokio::test(flavor = "multi_thread")]
+async fn can_install_solc_and_compile_version() {
+    let project = TempProject::dapptools().unwrap();
+    let version = Version::new(0, 8, 10);
+
+    project
+        .add_source(
+            "Contract",
+            format!(
+                r#"
+pragma solidity {};
+contract Contract {{ }}
+"#,
+                version.to_string()
+            ),
+        )
+        .unwrap();
+
+    remove_solc_if_exists(&version);
+
+    let compiled = project.compile().unwrap();
+    assert!(!compiled.has_compiler_errors());
+}
+
+#[tokio::test(flavor = "multi_thread")]
+async fn can_install_solc_and_compile_std_json_input_async() {
+    let tmp = TempProject::dapptools_init().unwrap();
+    tmp.assert_no_errors();
+    let source = tmp.list_source_files().into_iter().find(|p| p.ends_with("Dapp.t.sol")).unwrap();
+    let input = tmp.project().standard_json_input(source).unwrap();
+    let solc = &tmp.project().solc;
+
+    assert!(input.settings.remappings.contains(&"ds-test/=lib/ds-test/src/".parse().unwrap()));
+    let input: CompilerInput = input.into();
+    assert!(input.sources.contains_key(Path::new("lib/ds-test/src/test.sol")));
+
+    remove_solc_if_exists(&solc.version().expect("failed to get version"));
+
+    let out = solc.async_compile(&input).await.unwrap();
+    assert!(!out.has_error());
+    assert!(out.sources.contains_key("lib/ds-test/src/test.sol"));
+}