ethers_solc/compile/
mod.rs

1use crate::{
2    artifacts::Source,
3    error::{Result, SolcError},
4    utils, CompilerInput, CompilerOutput,
5};
6use once_cell::sync::Lazy;
7use semver::{Version, VersionReq};
8use serde::{de::DeserializeOwned, Deserialize, Serialize};
9use std::{
10    fmt,
11    path::{Path, PathBuf},
12    process::{Command, Output, Stdio},
13    str::FromStr,
14};
15pub mod many;
16pub mod output;
17pub use output::{contracts, info, sources};
18pub mod project;
19
20/// The name of the `solc` binary on the system
21pub const SOLC: &str = "solc";
22
23/// Support for configuring the EVM version
24/// <https://blog.soliditylang.org/2018/03/08/solidity-0.4.21-release-announcement/>
25pub const BYZANTIUM_SOLC: Version = Version::new(0, 4, 21);
26
27/// Bug fix for configuring the EVM version with Constantinople
28/// <https://blog.soliditylang.org/2018/03/08/solidity-0.4.21-release-announcement/>
29pub const CONSTANTINOPLE_SOLC: Version = Version::new(0, 4, 22);
30
31/// Petersburg support
32/// <https://blog.soliditylang.org/2019/03/05/solidity-0.5.5-release-announcement/>
33pub const PETERSBURG_SOLC: Version = Version::new(0, 5, 5);
34
35/// Istanbul support
36/// <https://blog.soliditylang.org/2019/12/09/solidity-0.5.14-release-announcement/>
37pub const ISTANBUL_SOLC: Version = Version::new(0, 5, 14);
38
39/// Berlin support
40/// <https://blog.soliditylang.org/2021/06/10/solidity-0.8.5-release-announcement/>
41pub const BERLIN_SOLC: Version = Version::new(0, 8, 5);
42
43/// London support
44/// <https://blog.soliditylang.org/2021/08/11/solidity-0.8.7-release-announcement/>
45pub const LONDON_SOLC: Version = Version::new(0, 8, 7);
46
47/// Paris support
48/// <https://blog.soliditylang.org/2023/02/01/solidity-0.8.18-release-announcement/>
49pub const PARIS_SOLC: Version = Version::new(0, 8, 18);
50
51/// Shanghai support
52/// <https://blog.soliditylang.org/2023/05/10/solidity-0.8.20-release-announcement/>
53pub const SHANGHAI_SOLC: Version = Version::new(0, 8, 20);
54
55// `--base-path` was introduced in 0.6.9 <https://github.com/ethereum/solidity/releases/tag/v0.6.9>
56pub static SUPPORTS_BASE_PATH: Lazy<VersionReq> =
57    Lazy::new(|| VersionReq::parse(">=0.6.9").unwrap());
58
59// `--include-path` was introduced in 0.8.8 <https://github.com/ethereum/solidity/releases/tag/v0.8.8>
60pub static SUPPORTS_INCLUDE_PATH: Lazy<VersionReq> =
61    Lazy::new(|| VersionReq::parse(">=0.8.8").unwrap());
62
63#[cfg(any(test, feature = "tests"))]
64use std::sync::Mutex;
65
66#[cfg(any(test, feature = "tests"))]
67#[allow(unused)]
68static LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
69
70/// take the lock in tests, we use this to enforce that
71/// a test does not run while a compiler version is being installed
72///
73/// This ensures that only one thread installs a missing `solc` exe.
74/// Instead of taking this lock in `Solc::blocking_install`, the lock should be taken before
75/// installation is detected.
76#[cfg(any(test, feature = "tests"))]
77#[allow(unused)]
78pub(crate) fn take_solc_installer_lock() -> std::sync::MutexGuard<'static, ()> {
79    LOCK.lock().unwrap()
80}
81
82/// A list of upstream Solc releases, used to check which version
83/// we should download.
84/// The boolean value marks whether there was an error accessing the release list
85#[cfg(all(feature = "svm-solc", not(target_arch = "wasm32")))]
86pub static RELEASES: Lazy<(svm::Releases, Vec<Version>, bool)> =
87    Lazy::new(|| match serde_json::from_str::<svm::Releases>(svm_builds::RELEASE_LIST_JSON) {
88        Ok(releases) => {
89            let sorted_versions = releases.clone().into_versions();
90            (releases, sorted_versions, true)
91        }
92        Err(err) => {
93            tracing::error!("{:?}", err);
94            Default::default()
95        }
96    });
97
98/// A `Solc` version is either installed (available locally) or can be downloaded, from the remote
99/// endpoint
100#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
101#[serde(untagged)]
102pub enum SolcVersion {
103    Installed(Version),
104    Remote(Version),
105}
106
107impl SolcVersion {
108    /// Whether this version is installed
109    pub fn is_installed(&self) -> bool {
110        matches!(self, SolcVersion::Installed(_))
111    }
112}
113
114impl AsRef<Version> for SolcVersion {
115    fn as_ref(&self) -> &Version {
116        match self {
117            SolcVersion::Installed(v) | SolcVersion::Remote(v) => v,
118        }
119    }
120}
121
122impl From<SolcVersion> for Version {
123    fn from(s: SolcVersion) -> Version {
124        match s {
125            SolcVersion::Installed(v) | SolcVersion::Remote(v) => v,
126        }
127    }
128}
129
130impl fmt::Display for SolcVersion {
131    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132        write!(f, "{}", self.as_ref())
133    }
134}
135
136/// Abstraction over `solc` command line utility
137///
138/// Supports sync and async functions.
139///
140/// By default the solc path is configured as follows, with descending priority:
141///   1. `SOLC_PATH` environment variable
142///   2. [svm](https://github.com/roynalnaruto/svm-rs)'s  `global_version` (set via `svm use
143///      <version>`), stored at `<svm_home>/.global_version`
144///   3. `solc` otherwise
145#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)]
146pub struct Solc {
147    /// Path to the `solc` executable
148    pub solc: PathBuf,
149    /// The base path to set when invoking solc, see also <https://docs.soliditylang.org/en/v0.8.11/path-resolution.html#base-path-and-include-paths>
150    pub base_path: Option<PathBuf>,
151    /// Additional arguments passed to the `solc` executable
152    pub args: Vec<String>,
153}
154
155impl Default for Solc {
156    fn default() -> Self {
157        if let Ok(solc) = std::env::var("SOLC_PATH") {
158            return Solc::new(solc)
159        }
160        #[cfg(not(target_arch = "wasm32"))]
161        {
162            if let Some(solc) = Solc::svm_global_version()
163                .and_then(|vers| Solc::find_svm_installed_version(vers.to_string()).ok())
164                .flatten()
165            {
166                return solc
167            }
168        }
169
170        Solc::new(SOLC)
171    }
172}
173
174impl fmt::Display for Solc {
175    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
176        write!(f, "{}", self.solc.display())?;
177        if !self.args.is_empty() {
178            write!(f, " {}", self.args.join(" "))?;
179        }
180        Ok(())
181    }
182}
183
184impl Solc {
185    /// A new instance which points to `solc`
186    pub fn new(path: impl Into<PathBuf>) -> Self {
187        Solc { solc: path.into(), base_path: None, args: Vec::new() }
188    }
189
190    /// Sets solc's base path
191    ///
192    /// Ref: <https://docs.soliditylang.org/en/v0.8.11/path-resolution.html#base-path-and-include-paths>
193    pub fn with_base_path(mut self, base_path: impl Into<PathBuf>) -> Self {
194        self.base_path = Some(base_path.into());
195        self
196    }
197
198    /// Adds an argument to pass to the `solc` command.
199    #[must_use]
200    pub fn arg<T: Into<String>>(mut self, arg: T) -> Self {
201        self.args.push(arg.into());
202        self
203    }
204
205    /// Adds multiple arguments to pass to the `solc`.
206    #[must_use]
207    pub fn args<I, S>(mut self, args: I) -> Self
208    where
209        I: IntoIterator<Item = S>,
210        S: Into<String>,
211    {
212        for arg in args {
213            self = self.arg(arg);
214        }
215        self
216    }
217
218    /// Returns the directory in which [svm](https://github.com/roynalnaruto/svm-rs) stores all versions
219    ///
220    /// This will be:
221    ///  `~/.svm` on unix, if it exists
222    /// - $XDG_DATA_HOME (~/.local/share/svm) if the svm folder does not exist.
223    #[cfg(not(target_arch = "wasm32"))]
224    pub fn svm_home() -> Option<PathBuf> {
225        match home::home_dir().map(|dir| dir.join(".svm")) {
226            Some(dir) => {
227                if !dir.exists() {
228                    dirs::data_dir().map(|dir| dir.join("svm"))
229                } else {
230                    Some(dir)
231                }
232            }
233            None => dirs::data_dir().map(|dir| dir.join("svm")),
234        }
235    }
236
237    /// Returns the `semver::Version` [svm](https://github.com/roynalnaruto/svm-rs)'s `.global_version` is currently set to.
238    ///  `global_version` is configured with (`svm use <version>`)
239    ///
240    /// This will read the version string (eg: "0.8.9") that the  `~/.svm/.global_version` file
241    /// contains
242    #[cfg(not(target_arch = "wasm32"))]
243    pub fn svm_global_version() -> Option<Version> {
244        let home = Self::svm_home()?;
245        let version = std::fs::read_to_string(home.join(".global_version")).ok()?;
246        Version::parse(&version).ok()
247    }
248
249    /// Returns the list of all solc instances installed at `SVM_HOME`
250    #[cfg(not(target_arch = "wasm32"))]
251    pub fn installed_versions() -> Vec<SolcVersion> {
252        Self::svm_home()
253            .map(|home| {
254                utils::installed_versions(home)
255                    .unwrap_or_default()
256                    .into_iter()
257                    .map(SolcVersion::Installed)
258                    .collect()
259            })
260            .unwrap_or_default()
261    }
262
263    /// Returns the list of all versions that are available to download and marking those which are
264    /// already installed.
265    #[cfg(all(feature = "svm-solc", not(target_arch = "wasm32")))]
266    pub fn all_versions() -> Vec<SolcVersion> {
267        let mut all_versions = Self::installed_versions();
268        let mut uniques = all_versions
269            .iter()
270            .map(|v| {
271                let v = v.as_ref();
272                (v.major, v.minor, v.patch)
273            })
274            .collect::<std::collections::HashSet<_>>();
275        all_versions.extend(
276            RELEASES
277                .1
278                .clone()
279                .into_iter()
280                .filter(|v| uniques.insert((v.major, v.minor, v.patch)))
281                .map(SolcVersion::Remote),
282        );
283        all_versions.sort_unstable();
284        all_versions
285    }
286
287    /// Returns the path for a [svm](https://github.com/roynalnaruto/svm-rs) installed version.
288    ///
289    /// # Example
290    /// ```no_run
291    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
292    ///  use ethers_solc::Solc;
293    /// let solc = Solc::find_svm_installed_version("0.8.9").unwrap();
294    /// assert_eq!(solc, Some(Solc::new("~/.svm/0.8.9/solc-0.8.9")));
295    /// # Ok(())
296    /// # }
297    /// ```
298    #[cfg(not(target_arch = "wasm32"))]
299    pub fn find_svm_installed_version(version: impl AsRef<str>) -> Result<Option<Self>> {
300        let version = version.as_ref();
301        let solc = Self::svm_home()
302            .ok_or_else(|| SolcError::msg("svm home dir not found"))?
303            .join(version)
304            .join(format!("solc-{version}"));
305
306        if !solc.is_file() {
307            return Ok(None)
308        }
309        Ok(Some(Solc::new(solc)))
310    }
311
312    /// Returns the path for a [svm](https://github.com/roynalnaruto/svm-rs) installed version.
313    ///
314    /// If the version is not installed yet, it will install it.
315    ///
316    /// # Example
317    /// ```no_run
318    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
319    ///  use ethers_solc::Solc;
320    /// let solc = Solc::find_or_install_svm_version("0.8.9").unwrap();
321    /// assert_eq!(solc, Solc::new("~/.svm/0.8.9/solc-0.8.9"));
322    /// # Ok(())
323    /// # }
324    /// ```
325    #[cfg(all(not(target_arch = "wasm32"), feature = "svm-solc"))]
326    pub fn find_or_install_svm_version(version: impl AsRef<str>) -> Result<Self> {
327        let version = version.as_ref();
328        if let Some(solc) = Solc::find_svm_installed_version(version)? {
329            Ok(solc)
330        } else {
331            Ok(Solc::blocking_install(&version.parse::<Version>()?)?)
332        }
333    }
334
335    /// Assuming the `versions` array is sorted, it returns the first element which satisfies
336    /// the provided [`VersionReq`]
337    pub fn find_matching_installation(
338        versions: &[Version],
339        required_version: &VersionReq,
340    ) -> Option<Version> {
341        // iterate in reverse to find the last match
342        versions.iter().rev().find(|version| required_version.matches(version)).cloned()
343    }
344
345    /// Given a Solidity source, it detects the latest compiler version which can be used
346    /// to build it, and returns it.
347    ///
348    /// If the required compiler version is not installed, it also proceeds to install it.
349    #[cfg(all(feature = "svm-solc", not(target_arch = "wasm32")))]
350    pub fn detect_version(source: &Source) -> Result<Version> {
351        // detects the required solc version
352        let sol_version = Self::source_version_req(source)?;
353        Self::ensure_installed(&sol_version)
354    }
355
356    /// Given a Solidity version requirement, it detects the latest compiler version which can be
357    /// used to build it, and returns it.
358    ///
359    /// If the required compiler version is not installed, it also proceeds to install it.
360    #[cfg(all(feature = "svm-solc", not(target_arch = "wasm32")))]
361    pub fn ensure_installed(sol_version: &VersionReq) -> Result<Version> {
362        #[cfg(any(test, feature = "tests"))]
363        let _lock = take_solc_installer_lock();
364
365        // load the local / remote versions
366        let versions = utils::installed_versions(svm::SVM_DATA_DIR.as_path()).unwrap_or_default();
367
368        let local_versions = Self::find_matching_installation(&versions, sol_version);
369        let remote_versions = Self::find_matching_installation(&RELEASES.1, sol_version);
370        // if there's a better upstream version than the one we have, install it
371        Ok(match (local_versions, remote_versions) {
372            (Some(local), None) => local,
373            (Some(local), Some(remote)) => {
374                if remote > local {
375                    Self::blocking_install(&remote)?;
376                    remote
377                } else {
378                    local
379                }
380            }
381            (None, Some(version)) => {
382                Self::blocking_install(&version)?;
383                version
384            }
385            // do nothing otherwise
386            _ => return Err(SolcError::VersionNotFound),
387        })
388    }
389
390    /// Parses the given source looking for the `pragma` definition and
391    /// returns the corresponding SemVer version requirement.
392    pub fn source_version_req(source: &Source) -> Result<VersionReq> {
393        let version =
394            utils::find_version_pragma(&source.content).ok_or(SolcError::PragmaNotFound)?;
395        Self::version_req(version.as_str())
396    }
397
398    /// Returns the corresponding SemVer version requirement for the solidity version.
399    ///
400    /// Note: This is a workaround for the fact that `VersionReq::parse` does not support whitespace
401    /// separators and requires comma separated operators. See [VersionReq].
402    pub fn version_req(version: &str) -> Result<VersionReq> {
403        let version = version.replace(' ', ",");
404
405        // Somehow, Solidity semver without an operator is considered to be "exact",
406        // but lack of operator automatically marks the operator as Caret, so we need
407        // to manually patch it? :shrug:
408        let exact = !matches!(&version[0..1], "*" | "^" | "=" | ">" | "<" | "~");
409        let mut version = VersionReq::parse(&version)?;
410        if exact {
411            version.comparators[0].op = semver::Op::Exact;
412        }
413
414        Ok(version)
415    }
416
417    /// Installs the provided version of Solc in the machine under the svm dir and returns the
418    /// [Solc] instance pointing to the installation.
419    ///
420    /// # Example
421    /// ```no_run
422    /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
423    ///  use ethers_solc::{Solc, ISTANBUL_SOLC};
424    ///  let solc = Solc::install(&ISTANBUL_SOLC).await.unwrap();
425    /// # Ok(())
426    /// # }
427    /// ```
428    #[cfg(all(feature = "svm-solc", not(target_arch = "wasm32")))]
429    pub async fn install(version: &Version) -> std::result::Result<Self, svm::SolcVmError> {
430        tracing::trace!("installing solc version \"{}\"", version);
431        crate::report::solc_installation_start(version);
432        let result = svm::install(version).await;
433        crate::report::solc_installation_success(version);
434        result.map(Solc::new)
435    }
436
437    /// Blocking version of `Self::install`
438    #[cfg(all(feature = "svm-solc", not(target_arch = "wasm32")))]
439    pub fn blocking_install(version: &Version) -> std::result::Result<Self, svm::SolcVmError> {
440        use crate::utils::RuntimeOrHandle;
441
442        tracing::trace!("blocking installing solc version \"{}\"", version);
443        crate::report::solc_installation_start(version);
444        // the async version `svm::install` is used instead of `svm::blocking_intsall`
445        // because the underlying `reqwest::blocking::Client` does not behave well
446        // in tokio rt. see https://github.com/seanmonstar/reqwest/issues/1017
447        cfg_if::cfg_if! {
448            if #[cfg(target_arch = "wasm32")] {
449                let installation = svm::blocking_install(version);
450            } else {
451                let installation = RuntimeOrHandle::new().block_on(svm::install(version));
452            }
453        };
454        match installation {
455            Ok(path) => {
456                crate::report::solc_installation_success(version);
457                Ok(Solc::new(path))
458            }
459            Err(err) => {
460                crate::report::solc_installation_error(version, &err.to_string());
461                Err(err)
462            }
463        }
464    }
465
466    /// Verify that the checksum for this version of solc is correct. We check against the SHA256
467    /// checksum from the build information published by [binaries.soliditylang.org](https://binaries.soliditylang.org/)
468    #[cfg(all(feature = "svm-solc", not(target_arch = "wasm32")))]
469    pub fn verify_checksum(&self) -> Result<()> {
470        let version = self.version_short()?;
471        let mut version_path = svm::version_path(version.to_string().as_str());
472        version_path.push(format!("solc-{}", version.to_string().as_str()));
473        tracing::trace!(target:"solc", "reading solc binary for checksum {:?}", version_path);
474        let content =
475            std::fs::read(&version_path).map_err(|err| SolcError::io(err, version_path.clone()))?;
476
477        if !RELEASES.2 {
478            // we skip checksum verification because the underlying request to fetch release info
479            // failed so we have nothing to compare against
480            return Ok(())
481        }
482
483        #[cfg(windows)]
484        {
485            // Prior to 0.7.2, binaries are released as exe files which are hard to verify: <https://github.com/foundry-rs/foundry/issues/5601>
486            // <https://binaries.soliditylang.org/windows-amd64/list.json>
487            const V0_7_2: Version = Version::new(0, 7, 2);
488            if version < V0_7_2 {
489                return Ok(())
490            }
491        }
492
493        use sha2::Digest;
494        let mut hasher = sha2::Sha256::new();
495        hasher.update(content);
496        let checksum_calc = &hasher.finalize()[..];
497
498        let checksum_found = &RELEASES
499            .0
500            .get_checksum(&version)
501            .ok_or_else(|| SolcError::ChecksumNotFound { version: version.clone() })?;
502
503        if checksum_calc == checksum_found {
504            Ok(())
505        } else {
506            let expected = hex::encode(checksum_found);
507            let detected = hex::encode(checksum_calc);
508            tracing:: warn!(target : "solc", "checksum mismatch for {:?}, expected {}, but found {} for file {:?}", version, expected, detected, version_path);
509            Err(SolcError::ChecksumMismatch { version, expected, detected, file: version_path })
510        }
511    }
512
513    /// Convenience function for compiling all sources under the given path
514    pub fn compile_source(&self, path: impl AsRef<Path>) -> Result<CompilerOutput> {
515        let path = path.as_ref();
516        let mut res: CompilerOutput = Default::default();
517        for input in CompilerInput::new(path)? {
518            let output = self.compile(&input)?;
519            res.merge(output)
520        }
521        Ok(res)
522    }
523
524    /// Same as [`Self::compile()`], but only returns those files which are included in the
525    /// `CompilerInput`.
526    ///
527    /// In other words, this removes those files from the `CompilerOutput` that are __not__ included
528    /// in the provided `CompilerInput`.
529    ///
530    /// # Example
531    ///
532    /// ```no_run
533    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
534    ///  use ethers_solc::{CompilerInput, Solc};
535    /// let solc = Solc::default();
536    /// let input = CompilerInput::new("./contracts")?[0].clone();
537    /// let output = solc.compile_exact(&input)?;
538    /// # Ok(())
539    /// # }
540    /// ```
541    pub fn compile_exact(&self, input: &CompilerInput) -> Result<CompilerOutput> {
542        let mut out = self.compile(input)?;
543        out.retain_files(input.sources.keys().filter_map(|p| p.to_str()));
544        Ok(out)
545    }
546
547    /// Run `solc --stand-json` and return the `solc`'s output as
548    /// `CompilerOutput`
549    ///
550    /// # Example
551    ///
552    /// ```no_run
553    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
554    ///  use ethers_solc::{CompilerInput, Solc};
555    /// let solc = Solc::default();
556    /// let input = CompilerInput::new("./contracts")?;
557    /// let output = solc.compile(&input)?;
558    /// # Ok(())
559    /// # }
560    /// ```
561    pub fn compile<T: Serialize>(&self, input: &T) -> Result<CompilerOutput> {
562        self.compile_as(input)
563    }
564
565    /// Run `solc --stand-json` and return the `solc`'s output as the given json
566    /// output
567    pub fn compile_as<T: Serialize, D: DeserializeOwned>(&self, input: &T) -> Result<D> {
568        let output = self.compile_output(input)?;
569        Ok(serde_json::from_slice(&output)?)
570    }
571
572    pub fn compile_output<T: Serialize>(&self, input: &T) -> Result<Vec<u8>> {
573        let mut cmd = Command::new(&self.solc);
574        if let Some(ref base_path) = self.base_path {
575            cmd.current_dir(base_path);
576            cmd.arg("--base-path").arg(base_path);
577        }
578        let mut child = cmd
579            .args(&self.args)
580            .arg("--standard-json")
581            .stdin(Stdio::piped())
582            .stderr(Stdio::piped())
583            .stdout(Stdio::piped())
584            .spawn()
585            .map_err(|err| SolcError::io(err, &self.solc))?;
586        let stdin = child.stdin.take().expect("Stdin exists.");
587        serde_json::to_writer(stdin, input)?;
588        compile_output(child.wait_with_output().map_err(|err| SolcError::io(err, &self.solc))?)
589    }
590
591    pub fn version_short(&self) -> Result<Version> {
592        let version = self.version()?;
593        Ok(Version::new(version.major, version.minor, version.patch))
594    }
595
596    /// Returns the version from the configured `solc`
597    pub fn version(&self) -> Result<Version> {
598        version_from_output(
599            Command::new(&self.solc)
600                .arg("--version")
601                .stdin(Stdio::piped())
602                .stderr(Stdio::piped())
603                .stdout(Stdio::piped())
604                .output()
605                .map_err(|err| SolcError::io(err, &self.solc))?,
606        )
607    }
608}
609
610#[cfg(feature = "async")]
611impl Solc {
612    /// Convenience function for compiling all sources under the given path
613    pub async fn async_compile_source(&self, path: impl AsRef<Path>) -> Result<CompilerOutput> {
614        self.async_compile(&CompilerInput::with_sources(Source::async_read_all_from(path).await?))
615            .await
616    }
617
618    /// Run `solc --stand-json` and return the `solc`'s output as
619    /// `CompilerOutput`
620    pub async fn async_compile<T: Serialize>(&self, input: &T) -> Result<CompilerOutput> {
621        self.async_compile_as(input).await
622    }
623
624    /// Run `solc --stand-json` and return the `solc`'s output as the given json
625    /// output
626    pub async fn async_compile_as<T: Serialize, D: DeserializeOwned>(
627        &self,
628        input: &T,
629    ) -> Result<D> {
630        let output = self.async_compile_output(input).await?;
631        Ok(serde_json::from_slice(&output)?)
632    }
633
634    pub async fn async_compile_output<T: Serialize>(&self, input: &T) -> Result<Vec<u8>> {
635        use tokio::io::AsyncWriteExt;
636        let content = serde_json::to_vec(input)?;
637        let mut cmd = tokio::process::Command::new(&self.solc);
638        if let Some(ref base_path) = self.base_path {
639            cmd.current_dir(base_path);
640        }
641        let mut child = cmd
642            .args(&self.args)
643            .arg("--standard-json")
644            .stdin(Stdio::piped())
645            .stderr(Stdio::piped())
646            .stdout(Stdio::piped())
647            .spawn()
648            .map_err(|err| SolcError::io(err, &self.solc))?;
649        let stdin = child.stdin.as_mut().unwrap();
650        stdin.write_all(&content).await.map_err(|err| SolcError::io(err, &self.solc))?;
651        stdin.flush().await.map_err(|err| SolcError::io(err, &self.solc))?;
652        compile_output(
653            child.wait_with_output().await.map_err(|err| SolcError::io(err, &self.solc))?,
654        )
655    }
656
657    pub async fn async_version(&self) -> Result<Version> {
658        version_from_output(
659            tokio::process::Command::new(&self.solc)
660                .arg("--version")
661                .stdin(Stdio::piped())
662                .stderr(Stdio::piped())
663                .stdout(Stdio::piped())
664                .spawn()
665                .map_err(|err| SolcError::io(err, &self.solc))?
666                .wait_with_output()
667                .await
668                .map_err(|err| SolcError::io(err, &self.solc))?,
669        )
670    }
671
672    /// Compiles all `CompilerInput`s with their associated `Solc`.
673    ///
674    /// This will buffer up to `n` `solc` processes and then return the `CompilerOutput`s in the
675    /// order in which they complete. No more than `n` futures will be buffered at any point in
676    /// time, and less than `n` may also be buffered depending on the state of each future.
677    ///
678    /// # Example
679    ///
680    /// Compile 2 `CompilerInput`s at once
681    ///
682    /// ```no_run
683    /// # async fn example() {
684    /// use ethers_solc::{CompilerInput, Solc};
685    /// let solc1 = Solc::default();
686    /// let solc2 = Solc::default();
687    /// let input1 = CompilerInput::new("contracts").unwrap()[0].clone();
688    /// let input2 = CompilerInput::new("src").unwrap()[0].clone();
689    ///
690    /// let outputs = Solc::compile_many([(solc1, input1), (solc2, input2)], 2).await.flattened().unwrap();
691    /// # }
692    /// ```
693    pub async fn compile_many<I>(jobs: I, n: usize) -> crate::many::CompiledMany
694    where
695        I: IntoIterator<Item = (Solc, CompilerInput)>,
696    {
697        use futures_util::stream::StreamExt;
698
699        let outputs = futures_util::stream::iter(
700            jobs.into_iter()
701                .map(|(solc, input)| async { (solc.async_compile(&input).await, solc, input) }),
702        )
703        .buffer_unordered(n)
704        .collect::<Vec<_>>()
705        .await;
706
707        crate::many::CompiledMany::new(outputs)
708    }
709}
710
711fn compile_output(output: Output) -> Result<Vec<u8>> {
712    if output.status.success() {
713        Ok(output.stdout)
714    } else {
715        Err(SolcError::solc_output(&output))
716    }
717}
718
719fn version_from_output(output: Output) -> Result<Version> {
720    if output.status.success() {
721        let stdout = String::from_utf8_lossy(&output.stdout);
722        let version = stdout
723            .lines()
724            .filter(|l| !l.trim().is_empty())
725            .last()
726            .ok_or_else(|| SolcError::msg("Version not found in Solc output"))?;
727        // NOTE: semver doesn't like `+` in g++ in build metadata which is invalid semver
728        Ok(Version::from_str(&version.trim_start_matches("Version: ").replace(".g++", ".gcc"))?)
729    } else {
730        Err(SolcError::solc_output(&output))
731    }
732}
733
734impl AsRef<Path> for Solc {
735    fn as_ref(&self) -> &Path {
736        &self.solc
737    }
738}
739
740impl<T: Into<PathBuf>> From<T> for Solc {
741    fn from(solc: T) -> Self {
742        Solc::new(solc.into())
743    }
744}
745
746#[cfg(test)]
747mod tests {
748    use super::*;
749    use crate::Artifact;
750
751    #[test]
752    fn test_version_parse() {
753        let req = Solc::version_req(">=0.6.2 <0.8.21").unwrap();
754        let semver_req: VersionReq = ">=0.6.2,<0.8.21".parse().unwrap();
755        assert_eq!(req, semver_req);
756    }
757
758    fn solc() -> Solc {
759        Solc::default()
760    }
761
762    #[test]
763    fn solc_version_works() {
764        solc().version().unwrap();
765    }
766
767    #[test]
768    fn can_parse_version_metadata() {
769        let _version = Version::from_str("0.6.6+commit.6c089d02.Linux.gcc").unwrap();
770    }
771
772    #[cfg(feature = "async")]
773    #[tokio::test]
774    async fn async_solc_version_works() {
775        let _version = solc().async_version().await.unwrap();
776    }
777
778    #[test]
779    fn solc_compile_works() {
780        let input = include_str!("../../test-data/in/compiler-in-1.json");
781        let input: CompilerInput = serde_json::from_str(input).unwrap();
782        let out = solc().compile(&input).unwrap();
783        let other = solc().compile(&serde_json::json!(input)).unwrap();
784        assert_eq!(out, other);
785    }
786
787    #[test]
788    fn solc_metadata_works() {
789        let input = include_str!("../../test-data/in/compiler-in-1.json");
790        let mut input: CompilerInput = serde_json::from_str(input).unwrap();
791        input.settings.push_output_selection("metadata");
792        let out = solc().compile(&input).unwrap();
793        for (_, c) in out.split().1.contracts_iter() {
794            assert!(c.metadata.is_some());
795        }
796    }
797
798    #[test]
799    fn can_compile_with_remapped_links() {
800        let input: CompilerInput =
801            serde_json::from_str(include_str!("../../test-data/library-remapping-in.json"))
802                .unwrap();
803        let out = solc().compile(&input).unwrap();
804        let (_, mut contracts) = out.split();
805        let contract = contracts.remove("LinkTest").unwrap();
806        let bytecode = &contract.get_bytecode().unwrap().object;
807        assert!(!bytecode.is_unlinked());
808    }
809
810    #[test]
811    fn can_compile_with_remapped_links_temp_dir() {
812        let input: CompilerInput =
813            serde_json::from_str(include_str!("../../test-data/library-remapping-in-2.json"))
814                .unwrap();
815        let out = solc().compile(&input).unwrap();
816        let (_, mut contracts) = out.split();
817        let contract = contracts.remove("LinkTest").unwrap();
818        let bytecode = &contract.get_bytecode().unwrap().object;
819        assert!(!bytecode.is_unlinked());
820    }
821
822    #[cfg(feature = "async")]
823    #[tokio::test]
824    async fn async_solc_compile_works() {
825        let input = include_str!("../../test-data/in/compiler-in-1.json");
826        let input: CompilerInput = serde_json::from_str(input).unwrap();
827        let out = solc().async_compile(&input).await.unwrap();
828        let other = solc().async_compile(&serde_json::json!(input)).await.unwrap();
829        assert_eq!(out, other);
830    }
831
832    #[cfg(feature = "async")]
833    #[tokio::test]
834    async fn async_solc_compile_works2() {
835        let input = include_str!("../../test-data/in/compiler-in-2.json");
836        let input: CompilerInput = serde_json::from_str(input).unwrap();
837        let out = solc().async_compile(&input).await.unwrap();
838        let other = solc().async_compile(&serde_json::json!(input)).await.unwrap();
839        assert_eq!(out, other);
840        let sync_out = solc().compile(&input).unwrap();
841        assert_eq!(out, sync_out);
842    }
843
844    #[test]
845    fn test_version_req() {
846        let versions = ["=0.1.2", "^0.5.6", ">=0.7.1", ">0.8.0"];
847        let sources = versions.iter().map(|version| source(version));
848
849        sources.zip(versions).for_each(|(source, version)| {
850            let version_req = Solc::source_version_req(&source).unwrap();
851            assert_eq!(version_req, VersionReq::from_str(version).unwrap());
852        });
853
854        // Solidity defines version ranges with a space, whereas the semver package
855        // requires them to be separated with a comma
856        let version_range = ">=0.8.0 <0.9.0";
857        let source = source(version_range);
858        let version_req = Solc::source_version_req(&source).unwrap();
859        assert_eq!(version_req, VersionReq::from_str(">=0.8.0,<0.9.0").unwrap());
860    }
861
862    #[test]
863    // This test might be a bit hard to maintain
864    #[cfg(all(feature = "svm-solc", not(target_arch = "wasm32")))]
865    fn test_detect_version() {
866        for (pragma, expected) in [
867            // pinned
868            ("=0.4.14", "0.4.14"),
869            // pinned too
870            ("0.4.14", "0.4.14"),
871            // The latest patch is 0.4.26
872            ("^0.4.14", "0.4.26"),
873            // range
874            (">=0.4.0 <0.5.0", "0.4.26"),
875            // latest - this has to be updated every time a new version is released.
876            // Requires the SVM version list to be updated as well.
877            (">=0.5.0", "0.8.24"),
878        ] {
879            let source = source(pragma);
880            let res = Solc::detect_version(&source).unwrap();
881            assert_eq!(res, Version::from_str(expected).unwrap());
882        }
883    }
884
885    #[test]
886    #[cfg(feature = "full")]
887    fn test_find_installed_version_path() {
888        // this test does not take the lock by default, so we need to manually
889        // add it here.
890        let _lock = LOCK.lock();
891        let ver = "0.8.6";
892        let version = Version::from_str(ver).unwrap();
893        if utils::installed_versions(svm::SVM_DATA_DIR.as_path())
894            .map(|versions| !versions.contains(&version))
895            .unwrap_or_default()
896        {
897            Solc::blocking_install(&version).unwrap();
898        }
899        let res = Solc::find_svm_installed_version(version.to_string()).unwrap().unwrap();
900        let expected = svm::SVM_DATA_DIR.join(ver).join(format!("solc-{ver}"));
901        assert_eq!(res.solc, expected);
902    }
903
904    #[test]
905    #[cfg(all(feature = "svm-solc", not(target_arch = "wasm32")))]
906    fn can_install_solc_in_tokio_rt() {
907        let version = Version::from_str("0.8.6").unwrap();
908        let rt = tokio::runtime::Runtime::new().unwrap();
909        let result = rt.block_on(async { Solc::blocking_install(&version) });
910        assert!(result.is_ok());
911    }
912
913    #[test]
914    fn does_not_find_not_installed_version() {
915        let ver = "1.1.1";
916        let version = Version::from_str(ver).unwrap();
917        let res = Solc::find_svm_installed_version(version.to_string()).unwrap();
918        assert!(res.is_none());
919    }
920
921    #[test]
922    fn test_find_latest_matching_installation() {
923        let versions = ["0.4.24", "0.5.1", "0.5.2"]
924            .iter()
925            .map(|version| Version::from_str(version).unwrap())
926            .collect::<Vec<_>>();
927
928        let required = VersionReq::from_str(">=0.4.24").unwrap();
929
930        let got = Solc::find_matching_installation(&versions, &required).unwrap();
931        assert_eq!(got, versions[2]);
932    }
933
934    #[test]
935    fn test_no_matching_installation() {
936        let versions = ["0.4.24", "0.5.1", "0.5.2"]
937            .iter()
938            .map(|version| Version::from_str(version).unwrap())
939            .collect::<Vec<_>>();
940
941        let required = VersionReq::from_str(">=0.6.0").unwrap();
942        let got = Solc::find_matching_installation(&versions, &required);
943        assert!(got.is_none());
944    }
945
946    ///// helpers
947
948    fn source(version: &str) -> Source {
949        Source::new(format!("pragma solidity {version};\n"))
950    }
951}