refactor(solc): rewrite compiler passes and cache change detection (#802)

* chore: clippy

* refactor: rewrite compiler passes and cache

* feat: more work on compile pipeline

* feat: add cache constructor

* add artifact filtering

* fine tune api

* feat: prepare version integration

* docs: more docs

* feat: add cacheentry2

* replace cacheentry types

* integrate new api

* docs: more docs

* feat: implement new output handler

* feat: integrate cached files in new compile pipeline

* refactor: more cache refactor

* docs: more docs

* feat: add source name mapping

* feat: implement new parallel solc

* refactor: do a little cleanup

* refactor: even more cleanup

* even more cleanup

* chore: make it compile

* chore: make it compile with all features

* chore: clippy fix

* feat: integrate new compiler pipeline

* docs: more docs

* refactor: move stuff around

* refactor: start deprecating output type

* chore: make it compile again

* chore(deps): bump solc version 0.2.0

* feat: unify output types

* cargo fix

* refactor: add contracts wrapper

* chore: replace ProjectCompileOutput

* docs: add more docs

* feat: add offline mode

* feat: more artifact helpers

* chore: cleanup cache

* chore: streamline types

* fix: better artifacts mapping

* chore: some cleanup

* chore: change artifact

* chore: add configure solc fn

* feat: add artifact reading

* feat: implement retain and extend

* feat: add cache extending

* feat: write to disk

* chore: make clippy happy

* feat: implement path mapping

* chore: nits

* feat: introduce states

* feat: add compiler state machine

* chore: move cache types to cache mod

* chore: make clippy happy

* feat: add debug derives

* fix: use resolved import source unit names

* fix: failing tests

* test: test multiple libs properly

* chore: make clippy happy

* chore: update CHANGELOG

* fix: doc tests

* fix: set offline mode correctly

* chore: make it compile again

* Update ethers-solc/src/artifacts.rs

Co-authored-by: Georgios Konstantopoulos <me@gakonst.com>

* feat: find remappings by default

* typos

* add eth_syncing RPC (#848)

* add eth_syncing RPC

* Changelo updated

* small comments

* Intermediate SyncingStatus

* fix(core): adjust Ganache for new cli output (#851)

* fix: review comments

* fix: cache relative path bug

* chore: add cache example

* chore: use absolute paths

* fix: remove overwritten files from cache

* fix: rustfmt

* chore: more helper functions

* chore: export AggregatedOutput

* feat: implement helper functions

* feat: even more helpers

* fix: failing doc tests

* refactor: remove source name map tracking

* fix: determine artifacts in ephemeral mode

* refactor: allowed paths should not fail

Co-authored-by: Georgios Konstantopoulos <me@gakonst.com>
Co-authored-by: rakita <rakita@users.noreply.github.com>
Co-authored-by: wolflo <33909953+wolflo@users.noreply.github.com>
This commit is contained in:
Matthias Seitz 2022-02-04 17:20:24 +01:00 committed by GitHub
parent 5005a3621a
commit b295d73c4a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 2777 additions and 1429 deletions

View File

@ -44,6 +44,10 @@
### Unreleased
- Total revamp of the `Project::compile` pipeline
[#802](https://github.com/gakonst/ethers-rs/pull/802)
- Support multiple versions of compiled contracts
- Breaking: deprecate hardhat cache file compatibility, cache file now tracks artifact paths and their versions
- Fix flatten replacement target location
[#846](https://github.com/gakonst/ethers-rs/pull/846)
- Fix duplicate files during flattening

40
Cargo.lock generated
View File

@ -1056,6 +1056,19 @@ dependencies = [
"cfg-if 1.0.0",
]
[[package]]
name = "env_logger"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3"
dependencies = [
"atty",
"humantime",
"log",
"regex",
"termcolor",
]
[[package]]
name = "eth-keystore"
version = "0.3.0"
@ -1370,11 +1383,12 @@ dependencies = [
[[package]]
name = "ethers-solc"
version = "0.1.0"
version = "0.2.0"
dependencies = [
"colored",
"criterion",
"dunce",
"env_logger",
"ethers-core",
"fs_extra",
"futures-util",
@ -1386,6 +1400,7 @@ dependencies = [
"num_cpus",
"once_cell",
"pretty_assertions",
"rand 0.8.4",
"rayon",
"regex",
"semver",
@ -1399,6 +1414,7 @@ dependencies = [
"tiny-keccak",
"tokio",
"tracing",
"tracing-subscriber",
"walkdir",
]
@ -1793,6 +1809,12 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
[[package]]
name = "humantime"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "hyper"
version = "0.14.16"
@ -2062,6 +2084,15 @@ dependencies = [
"cfg-if 1.0.0",
]
[[package]]
name = "matchers"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
dependencies = [
"regex-automata",
]
[[package]]
name = "matches"
version = "0.1.9"
@ -2848,6 +2879,9 @@ name = "regex-automata"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
dependencies = [
"regex-syntax",
]
[[package]]
name = "regex-syntax"
@ -3848,9 +3882,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5312f325fe3588e277415f5a6cca1f4ccad0f248c4cd5a4bd33032d7286abc22"
dependencies = [
"ansi_term",
"lazy_static",
"matchers",
"regex",
"sharded-slab",
"smallvec",
"thread_local",
"tracing",
"tracing-core",
"tracing-log",
]

View File

@ -88,7 +88,7 @@ ethers-core = { version = "^0.6.0", default-features = false, path = "./ethers-c
ethers-providers = { version = "^0.6.0", default-features = false, path = "./ethers-providers" }
ethers-signers = { version = "^0.6.0", default-features = false, path = "./ethers-signers" }
ethers-middleware = { version = "^0.6.0", default-features = false, path = "./ethers-middleware" }
ethers-solc = { version = "^0.1.0", default-features = false, path = "./ethers-solc" }
ethers-solc = { version = "^0.2.0", default-features = false, path = "./ethers-solc" }
ethers-etherscan = { version = "^0.2.0", default-features = false, path = "./ethers-etherscan" }
[dev-dependencies]

View File

@ -32,7 +32,7 @@ ethers-contract-abigen = { version = "^0.6.0", path = "ethers-contract-abigen" }
ethers-contract-derive = { version = "^0.6.0", path = "ethers-contract-derive" }
ethers-core = { version = "^0.6.0", path = "../ethers-core", default-features = false, features = ["eip712"]}
ethers-derive-eip712 = { version = "^0.2.0", path = "../ethers-core/ethers-derive-eip712"}
ethers-solc = { version = "^0.1.0", path = "../ethers-solc", default-features = false }
ethers-solc = { version = "^0.2.0", path = "../ethers-solc", default-features = false }
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
tokio = { version = "1.5", default-features = false, features = ["macros"] }

View File

@ -42,7 +42,7 @@ hex = { version = "0.4.3", default-features = false, features = ["std"] }
rand = { version = "0.8.4", default-features = false }
ethers-providers = { version = "^0.6.0", path = "../ethers-providers", default-features = false, features = ["ws", "rustls"] }
once_cell = "1.8.0"
ethers-solc = { version = "^0.1.0", path = "../ethers-solc", default-features = false }
ethers-solc = { version = "^0.2.0", path = "../ethers-solc", default-features = false }
serial_test = "0.5.1"
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]

View File

@ -1,6 +1,6 @@
[package]
name = "ethers-solc"
version = "0.1.0"
version = "0.2.0"
authors = ["Matthias Seitz <matthias.seitz@outlook.de>", "Georgios Konstantopoulos <me@gakonst.com>"]
license = "MIT OR Apache-2.0"
edition = "2018"
@ -17,7 +17,7 @@ keywords = ["ethereum", "web3", "solc", "solidity", "ethers"]
ethers-core = { version = "^0.6.0", path = "../ethers-core", default-features = false }
serde_json = "1.0.68"
serde = { version = "1.0.130", features = ["derive"] }
semver = "1.0.4"
semver = { version = "1.0.4", features = ["serde"] }
walkdir = "2.3.2"
tokio = { version = "1.15.0", default-features = false, features = ["process", "io-util", "fs", "time"], optional = true }
futures-util = { version = "^0.3", optional = true }
@ -50,6 +50,9 @@ getrandom = { version = "0.2", features = ["js"] }
[dev-dependencies]
criterion = { version = "0.3", features = ["async_tokio"] }
env_logger = "*"
tracing-subscriber = {version = "0.3", default-features = false, features = ["env-filter", "fmt"]}
rand = "0.8.4"
pretty_assertions = "1.1.0"
tempfile = "3.3.0"
tokio = { version = "1.15.0", features = ["full"] }

View File

@ -0,0 +1,567 @@
//! Output artifact handling
use crate::{
artifacts::{CompactContract, CompactContractBytecode, Contract, FileToContractsMap},
contracts::VersionedContracts,
error::Result,
utils, HardhatArtifact, ProjectPathsConfig, SolcError,
};
use ethers_core::{abi::Abi, types::Bytes};
use semver::Version;
use serde::{de::DeserializeOwned, Serialize};
use std::{
collections::btree_map::BTreeMap,
fmt, fs, io,
path::{Path, PathBuf},
};
/// Represents an artifact file representing a [`crate::Contract`]
#[derive(Debug, Clone, PartialEq)]
pub struct ArtifactFile<T> {
/// The Artifact that was written
pub artifact: T,
/// path to the file where the `artifact` was written to
pub file: PathBuf,
/// `solc` version that produced this artifact
pub version: Version,
}
impl<T: Serialize> ArtifactFile<T> {
/// Writes the given contract to the `out` path creating all parent directories
pub fn write(&self) -> Result<()> {
utils::create_parent_dir_all(&self.file)?;
fs::write(&self.file, serde_json::to_vec_pretty(&self.artifact)?)
.map_err(|err| SolcError::io(err, &self.file))?;
Ok(())
}
}
impl<T> ArtifactFile<T> {
/// Sets the file to `root` adjoined to `self.file`.
pub fn join(&mut self, root: impl AsRef<Path>) {
self.file = root.as_ref().join(&self.file);
}
/// Removes `base` from the artifact's path
pub fn strip_prefix(&mut self, base: impl AsRef<Path>) {
if let Ok(prefix) = self.file.strip_prefix(base) {
self.file = prefix.to_path_buf();
}
}
}
/// local helper type alias `file name -> (contract name -> Vec<..>)`
pub(crate) type ArtifactsMap<T> = FileToContractsMap<Vec<ArtifactFile<T>>>;
/// Represents a set of Artifacts
#[derive(Debug, Clone, PartialEq)]
pub struct Artifacts<T>(pub ArtifactsMap<T>);
impl<T> From<ArtifactsMap<T>> for Artifacts<T> {
fn from(m: ArtifactsMap<T>) -> Self {
Self(m)
}
}
impl<'a, T> IntoIterator for &'a Artifacts<T> {
type Item = (&'a String, &'a BTreeMap<String, Vec<ArtifactFile<T>>>);
type IntoIter =
std::collections::btree_map::Iter<'a, String, BTreeMap<String, Vec<ArtifactFile<T>>>>;
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}
impl<T> IntoIterator for Artifacts<T> {
type Item = (String, BTreeMap<String, Vec<ArtifactFile<T>>>);
type IntoIter =
std::collections::btree_map::IntoIter<String, BTreeMap<String, Vec<ArtifactFile<T>>>>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl<T> Default for Artifacts<T> {
fn default() -> Self {
Self(Default::default())
}
}
impl<T> AsRef<ArtifactsMap<T>> for Artifacts<T> {
fn as_ref(&self) -> &ArtifactsMap<T> {
&self.0
}
}
impl<T> AsMut<ArtifactsMap<T>> for Artifacts<T> {
fn as_mut(&mut self) -> &mut ArtifactsMap<T> {
&mut self.0
}
}
impl<T: Serialize> Artifacts<T> {
/// Writes all artifacts into the given `artifacts_root` folder
pub fn write_all(&self) -> Result<()> {
for artifact in self.artifact_files() {
artifact.write()?;
}
Ok(())
}
}
impl<T> Artifacts<T> {
pub fn into_inner(self) -> ArtifactsMap<T> {
self.0
}
/// Sets the artifact files location to `root` adjoined to `self.file`.
pub fn join_all(&mut self, root: impl AsRef<Path>) -> &mut Self {
let root = root.as_ref();
self.artifact_files_mut().for_each(|artifact| artifact.join(root));
self
}
/// Removes `base` from all artifacts
pub fn strip_prefix_all(&mut self, base: impl AsRef<Path>) -> &mut Self {
let base = base.as_ref();
self.artifact_files_mut().for_each(|artifact| artifact.strip_prefix(base));
self
}
/// Returns all `ArtifactFile`s for the contract with the matching name
fn get_contract_artifact_files(&self, contract_name: &str) -> Option<&Vec<ArtifactFile<T>>> {
self.0.values().find_map(|all| all.get(contract_name))
}
/// Returns true if this type contains an artifact with the given path for the given contract
pub fn has_contract_artifact(&self, contract_name: &str, artifact_path: &Path) -> bool {
self.get_contract_artifact_files(contract_name)
.map(|artifacts| artifacts.iter().any(|artifact| artifact.file == artifact_path))
.unwrap_or_default()
}
/// Returns true if this type contains an artifact with the given path
pub fn has_artifact(&self, artifact_path: &Path) -> bool {
self.artifact_files().any(|artifact| artifact.file == artifact_path)
}
/// Iterate over all artifact files
pub fn artifact_files(&self) -> impl Iterator<Item = &ArtifactFile<T>> {
self.0.values().flat_map(|c| c.values().flat_map(|artifacts| artifacts.iter()))
}
/// Iterate over all artifact files
pub fn artifact_files_mut(&mut self) -> impl Iterator<Item = &mut ArtifactFile<T>> {
self.0.values_mut().flat_map(|c| c.values_mut().flat_map(|artifacts| artifacts.iter_mut()))
}
/// Returns an iterator over _all_ artifacts and `<file name:contract name>`
pub fn into_artifacts<O: ArtifactOutput<Artifact = T>>(
self,
) -> impl Iterator<Item = (String, T)> {
self.0.into_values().flat_map(|contract_artifacts| {
contract_artifacts.into_iter().flat_map(|(_contract_name, artifacts)| {
artifacts.into_iter().filter_map(|artifact| {
O::contract_name(&artifact.file).map(|name| {
(
format!(
"{}:{}",
artifact.file.file_name().unwrap().to_string_lossy(),
name
),
artifact.artifact,
)
})
})
})
})
}
/// Returns an iterator that yields the tuple `(file, contract name, artifact)`
///
/// **NOTE** this returns the path as is
pub fn into_artifacts_with_files(self) -> impl Iterator<Item = (String, String, T)> {
self.0.into_iter().flat_map(|(f, contract_artifacts)| {
contract_artifacts.into_iter().flat_map(move |(name, artifacts)| {
let contract_name = name;
let file = f.clone();
artifacts
.into_iter()
.map(move |artifact| (file.clone(), contract_name.clone(), artifact.artifact))
})
})
}
/// Strips the given prefix from all artifact file paths to make them relative to the given
/// `root` argument
pub fn into_stripped_file_prefixes(self, base: impl AsRef<Path>) -> Self {
let base = base.as_ref();
let artifacts = self
.0
.into_iter()
.map(|(file, c)| {
let file_path = Path::new(&file);
if let Ok(p) = file_path.strip_prefix(base) {
(p.to_string_lossy().to_string(), c)
} else {
(file, c)
}
})
.collect();
Artifacts(artifacts)
}
/// Finds the first artifact `T` with a matching contract name
pub fn find(&self, contract_name: impl AsRef<str>) -> Option<&T> {
let contract_name = contract_name.as_ref();
self.0.iter().find_map(|(_file, contracts)| {
contracts.get(contract_name).and_then(|c| c.get(0).map(|a| &a.artifact))
})
}
/// Removes the first artifact `T` with a matching contract name
///
/// *Note:* if there are multiple artifacts (contract compiled with different solc) then this
/// returns the first artifact in that set
pub fn remove(&mut self, contract_name: impl AsRef<str>) -> Option<T> {
let contract_name = contract_name.as_ref();
self.0.iter_mut().find_map(|(_file, contracts)| {
let mut artifact = None;
if let Some((c, mut artifacts)) = contracts.remove_entry(contract_name) {
if !artifacts.is_empty() {
artifact = Some(artifacts.remove(0).artifact);
}
if !artifacts.is_empty() {
contracts.insert(c, artifacts);
}
}
artifact
})
}
}
/// A trait representation for a [`crate::Contract`] artifact
pub trait Artifact {
/// Returns the artifact's `Abi` and bytecode
fn into_inner(self) -> (Option<Abi>, Option<Bytes>);
/// Turns the artifact into a container type for abi, compact bytecode and deployed bytecode
fn into_compact_contract(self) -> CompactContract;
/// Turns the artifact into a container type for abi, full bytecode and deployed bytecode
fn into_contract_bytecode(self) -> CompactContractBytecode;
/// Returns the contents of this type as a single tuple of abi, bytecode and deployed bytecode
fn into_parts(self) -> (Option<Abi>, Option<Bytes>, Option<Bytes>);
/// Same as [`Self::into_parts()`] but returns `Err` if an element is `None`
fn try_into_parts(self) -> Result<(Abi, Bytes, Bytes)>
where
Self: Sized,
{
let (abi, bytecode, deployed_bytecode) = self.into_parts();
Ok((
abi.ok_or_else(|| SolcError::msg("abi missing"))?,
bytecode.ok_or_else(|| SolcError::msg("bytecode missing"))?,
deployed_bytecode.ok_or_else(|| SolcError::msg("deployed bytecode missing"))?,
))
}
}
impl<T> Artifact for T
where
T: Into<CompactContractBytecode> + Into<CompactContract>,
{
fn into_inner(self) -> (Option<Abi>, Option<Bytes>) {
let artifact = self.into_compact_contract();
(artifact.abi, artifact.bin.and_then(|bin| bin.into_bytes()))
}
fn into_compact_contract(self) -> CompactContract {
self.into()
}
fn into_contract_bytecode(self) -> CompactContractBytecode {
self.into()
}
fn into_parts(self) -> (Option<Abi>, Option<Bytes>, Option<Bytes>) {
self.into_compact_contract().into_parts()
}
}
/// Handler invoked with the output of `solc`
///
/// Implementers of this trait are expected to take care of [`crate::Contract`] to
/// [`crate::ArtifactOutput::Artifact`] conversion and how that `Artifact` type is stored on disk,
/// this includes artifact file location and naming.
///
/// Depending on the [`crate::Project`] contracts and their compatible versions,
/// [`crate::ProjectCompiler::compile()`] may invoke different `solc` executables on the same
/// solidity file leading to multiple [`crate::CompilerOutput`]s for the same `.sol` file.
/// In addition to the `solidity file` to `contract` relationship (1-N*)
/// [`crate::VersionedContracts`] also tracks the `contract` to (`artifact` + `solc version`)
/// relationship (1-N+).
pub trait ArtifactOutput {
/// Represents the artifact that will be stored for a `Contract`
type Artifact: Artifact + DeserializeOwned + Serialize + fmt::Debug;
/// Handle the aggregated set of compiled contracts from the solc [`crate::CompilerOutput`].
///
/// This will be invoked with all aggregated contracts from (multiple) solc `CompilerOutput`.
/// See [`crate::AggregatedCompilerOutput`]
fn on_output(
contracts: &VersionedContracts,
layout: &ProjectPathsConfig,
) -> Result<Artifacts<Self::Artifact>> {
let mut artifacts = Self::output_to_artifacts(contracts);
artifacts.join_all(&layout.artifacts);
artifacts.write_all()?;
Self::write_extras(contracts, layout)?;
Ok(artifacts)
}
/// Writes additional files for the contracts if the included in the `Contract`, such as `ir`,
/// `ewasm`, `iropt`.
///
/// By default, these fields are _not_ enabled in the [`crate::Settings`], see
/// [`crate::Settings::default_output_selection()`], and the respective fields of the
/// [`Contract`] will `None`. If they'll be manually added to the `output_selection`, then
/// we're also creating individual files for this output, such as `Greeter.iropt`,
/// `Gretter.ewasm`
fn write_extras(contracts: &VersionedContracts, layout: &ProjectPathsConfig) -> Result<()> {
for (file, contracts) in contracts.as_ref().iter() {
for (name, versioned_contracts) in contracts {
for c in versioned_contracts {
let artifact_path = if versioned_contracts.len() > 1 {
Self::output_file_versioned(file, name, &c.version)
} else {
Self::output_file(file, name)
};
let file = layout.artifacts.join(artifact_path);
utils::create_parent_dir_all(&file)?;
if let Some(iropt) = &c.contract.ir_optimized {
fs::write(&file.with_extension("iropt"), iropt)
.map_err(|err| SolcError::io(err, file.with_extension("iropt")))?
}
if let Some(ir) = &c.contract.ir {
fs::write(&file.with_extension("ir"), ir)
.map_err(|err| SolcError::io(err, file.with_extension("ir")))?
}
if let Some(ewasm) = &c.contract.ewasm {
fs::write(
&file.with_extension("ewasm"),
serde_json::to_vec_pretty(&ewasm)?,
)
.map_err(|err| SolcError::io(err, file.with_extension("ewasm")))?;
}
if let Some(evm) = &c.contract.evm {
if let Some(asm) = &evm.assembly {
fs::write(&file.with_extension("asm"), asm)
.map_err(|err| SolcError::io(err, file.with_extension("asm")))?
}
}
}
}
}
Ok(())
}
/// Returns the file name for the contract's artifact
/// `Greeter.json`
fn output_file_name(name: impl AsRef<str>) -> PathBuf {
format!("{}.json", name.as_ref()).into()
}
/// Returns the file name for the contract's artifact and the given version
/// `Greeter.0.8.11.json`
fn output_file_name_versioned(name: impl AsRef<str>, version: &Version) -> PathBuf {
format!("{}.{}.{}.{}.json", name.as_ref(), version.major, version.minor, version.patch)
.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<Path>, name: impl AsRef<str>) -> 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))
}
/// Returns the path to the contract's artifact location based on the contract's file, name and
/// version
///
/// This returns `contract.sol/contract.0.8.11.json` by default
fn output_file_versioned(
contract_file: impl AsRef<Path>,
name: impl AsRef<str>,
version: &Version,
) -> PathBuf {
let name = name.as_ref();
contract_file
.as_ref()
.file_name()
.map(Path::new)
.map(|p| p.join(Self::output_file_name_versioned(name, version)))
.unwrap_or_else(|| Self::output_file_name_versioned(name, version))
}
/// 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<Path>) -> Option<String> {
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<Path>,
name: impl AsRef<str>,
root: impl AsRef<Path>,
) -> bool {
root.as_ref().join(Self::output_file(contract_file, name)).exists()
}
/// Read the artifact that's stored at the given path
///
/// # Errors
///
/// Returns an error if
/// - The file does not exist
/// - The file's content couldn't be deserialized into the `Artifact` type
fn read_cached_artifact(path: impl AsRef<Path>) -> Result<Self::Artifact> {
let path = path.as_ref();
let file = fs::File::open(path).map_err(|err| SolcError::io(err, path))?;
let file = io::BufReader::new(file);
Ok(serde_json::from_reader(file)?)
}
/// Read the cached artifacts that are located the paths the iterator yields
///
/// See [`Self::read_cached_artifact()`]
fn read_cached_artifacts<T, I>(files: I) -> Result<BTreeMap<PathBuf, Self::Artifact>>
where
I: IntoIterator<Item = T>,
T: Into<PathBuf>,
{
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
///
/// This is the core conversion function that takes care of converting a `Contract` into the
/// associated `Artifact` type
fn contract_to_artifact(_file: &str, _name: &str, contract: Contract) -> Self::Artifact;
/// Convert the compiler output into a set of artifacts
///
/// **Note:** This does only convert, but _NOT_ write the artifacts to disk, See
/// [`Self::on_output()`]
fn output_to_artifacts(contracts: &VersionedContracts) -> Artifacts<Self::Artifact> {
let mut artifacts = ArtifactsMap::new();
for (file, contracts) in contracts.as_ref().iter() {
let mut entries = BTreeMap::new();
for (name, versioned_contracts) in contracts {
let mut contracts = Vec::with_capacity(versioned_contracts.len());
// check if the same contract compiled with multiple solc versions
for contract in versioned_contracts {
let artifact_path = if versioned_contracts.len() > 1 {
Self::output_file_versioned(file, name, &contract.version)
} else {
Self::output_file(file, name)
};
let artifact =
Self::contract_to_artifact(file, name, contract.contract.clone());
contracts.push(ArtifactFile {
artifact,
file: artifact_path,
version: contract.version.clone(),
});
}
entries.insert(name.to_string(), contracts);
}
artifacts.insert(file.to_string(), entries);
}
Artifacts(artifacts)
}
}
/// 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 = CompactContractBytecode;
fn contract_to_artifact(_file: &str, _name: &str, contract: Contract) -> Self::Artifact {
Self::Artifact::from(contract)
}
}
/// An Artifacts handler implementation that works the same as `MinimalCombinedArtifacts` but also
/// supports reading hardhat artifacts if an initial attempt to deserialize an artifact failed
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub struct MinimalCombinedArtifactsHardhatFallback;
impl ArtifactOutput for MinimalCombinedArtifactsHardhatFallback {
type Artifact = CompactContractBytecode;
fn on_output(
output: &VersionedContracts,
layout: &ProjectPathsConfig,
) -> Result<Artifacts<Self::Artifact>> {
MinimalCombinedArtifacts::on_output(output, layout)
}
fn read_cached_artifact(path: impl AsRef<Path>) -> Result<Self::Artifact> {
let path = path.as_ref();
let content = fs::read_to_string(path).map_err(|err| SolcError::io(err, path))?;
if let Ok(a) = serde_json::from_str(&content) {
Ok(a)
} else {
tracing::error!("Failed to deserialize compact artifact");
tracing::trace!("Fallback to hardhat artifact deserialization");
let artifact = serde_json::from_str::<HardhatArtifact>(&content)?;
tracing::trace!("successfully deserialized hardhat artifact");
Ok(artifact.into_contract_bytecode())
}
}
fn contract_to_artifact(file: &str, name: &str, contract: Contract) -> Self::Artifact {
MinimalCombinedArtifacts::contract_to_artifact(file, name, contract)
}
}

View File

@ -22,10 +22,20 @@ use crate::{
use ethers_core::abi::Address;
use serde::{de::Visitor, Deserialize, Deserializer, Serialize, Serializer};
/// Solidity files are made up of multiple `source units`, a solidity contract is such a `source
/// unit`, therefore a solidity file can contain multiple contracts: (1-N*) relationship.
///
/// This types represents this mapping as `file name -> (contract name -> T)`, where the generic is
/// intended to represent contract specific information, like [`Contract`] itself, See [`Contracts`]
pub type FileToContractsMap<T> = BTreeMap<String, BTreeMap<String, T>>;
/// file -> (contract name -> Contract)
pub type Contracts = FileToContractsMap<Contract>;
/// An ordered list of files and their source
pub type Sources = BTreeMap<PathBuf, Source>;
pub type Contracts = BTreeMap<String, BTreeMap<String, Contract>>;
pub type VersionedSources = BTreeMap<Solc, (Version, Sources)>;
/// Input type `solc` expects
#[derive(Clone, Debug, Serialize, Deserialize)]
@ -547,22 +557,7 @@ impl CompilerOutput {
})
}
pub fn diagnostics<'a>(&'a self, ignored_error_codes: &'a [u64]) -> OutputDiagnostics {
OutputDiagnostics { compiler_output: self, ignored_error_codes }
}
/// Finds the _first_ contract with the given name
///
/// # Example
///
/// ```
/// use ethers_solc::Project;
/// use ethers_solc::artifacts::*;
/// # fn demo(project: Project) {
/// let output = project.compile().unwrap().output();
/// let contract = output.find("Greeter").unwrap();
/// # }
/// ```
pub fn find(&self, contract: impl AsRef<str>) -> Option<CompactContractRef> {
let contract_name = contract.as_ref();
self.contracts_iter().find_map(|(name, contract)| {
@ -571,17 +566,6 @@ impl CompilerOutput {
}
/// Finds the first contract with the given name and removes it from the set
///
/// # Example
///
/// ```
/// use ethers_solc::Project;
/// use ethers_solc::artifacts::*;
/// # fn demo(project: Project) {
/// let mut output = project.compile().unwrap().output();
/// let contract = output.remove("Greeter").unwrap();
/// # }
/// ```
pub fn remove(&mut self, contract: impl AsRef<str>) -> Option<Contract> {
let contract_name = contract.as_ref();
self.contracts.values_mut().find_map(|c| c.remove(contract_name))
@ -608,16 +592,6 @@ impl CompilerOutput {
/// Returns the output's source files and contracts separately, wrapped in helper types that
/// provide several helper methods
///
/// # Example
///
/// ```
/// use ethers_solc::Project;
/// # fn demo(project: Project) {
/// let output = project.compile().unwrap().output();
/// let (sources, contracts) = output.split();
/// # }
/// ```
pub fn split(self) -> (SourceFiles, OutputContracts) {
(SourceFiles(self.sources), OutputContracts(self.contracts))
}
@ -629,17 +603,6 @@ pub struct OutputContracts(pub Contracts);
impl OutputContracts {
/// Returns an iterator over all contracts and their source names.
///
/// ```
/// use std::collections::BTreeMap;
/// use ethers_solc::{ artifacts::*, Artifact };
/// # fn demo(contracts: OutputContracts) {
/// let contracts: BTreeMap<String, CompactContractSome> = contracts
/// .into_contracts()
/// .map(|(k, c)| (k, c.into_compact_contract().unwrap()))
/// .collect();
/// # }
/// ```
pub fn into_contracts(self) -> impl Iterator<Item = (String, Contract)> {
self.0.into_values().flatten()
}
@ -650,17 +613,6 @@ impl OutputContracts {
}
/// Finds the _first_ contract with the given name
///
/// # Example
///
/// ```
/// use ethers_solc::Project;
/// use ethers_solc::artifacts::*;
/// # fn demo(project: Project) {
/// let output = project.compile().unwrap().output();
/// let contract = output.find("Greeter").unwrap();
/// # }
/// ```
pub fn find(&self, contract: impl AsRef<str>) -> Option<CompactContractRef> {
let contract_name = contract.as_ref();
self.contracts_iter().find_map(|(name, contract)| {
@ -669,87 +621,12 @@ impl OutputContracts {
}
/// Finds the first contract with the given name and removes it from the set
///
/// # Example
///
/// ```
/// use ethers_solc::Project;
/// use ethers_solc::artifacts::*;
/// # fn demo(project: Project) {
/// let (_, mut contracts) = project.compile().unwrap().output().split();
/// let contract = contracts.remove("Greeter").unwrap();
/// # }
/// ```
pub fn remove(&mut self, contract: impl AsRef<str>) -> Option<Contract> {
let contract_name = contract.as_ref();
self.0.values_mut().find_map(|c| c.remove(contract_name))
}
}
/// Helper type to implement display for solc errors
#[derive(Clone, Debug)]
pub struct OutputDiagnostics<'a> {
compiler_output: &'a CompilerOutput,
ignored_error_codes: &'a [u64],
}
impl<'a> OutputDiagnostics<'a> {
/// Returns true if there is at least one error of high severity
pub fn has_error(&self) -> bool {
self.compiler_output.has_error()
}
/// Returns true if there is at least one warning
pub fn has_warning(&self) -> bool {
self.compiler_output.has_warning(self.ignored_error_codes)
}
fn is_test<T: AsRef<str>>(&self, contract_path: T) -> bool {
if contract_path.as_ref().ends_with(".t.sol") {
return true
}
self.compiler_output.find(&contract_path).map_or(false, |contract| {
contract.abi.map_or(false, |abi| abi.functions.contains_key("IS_TEST"))
})
}
}
impl<'a> fmt::Display for OutputDiagnostics<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.has_error() {
f.write_str("Compiler run failed")?;
} else if self.has_warning() {
f.write_str("Compiler run successful (with warnings)")?;
} else {
f.write_str("Compiler run successful")?;
}
for err in &self.compiler_output.errors {
if err.severity.is_warning() {
let is_ignored = err.error_code.as_ref().map_or(false, |code| {
if let Some(source_location) = &err.source_location {
// we ignore spdx and contract size warnings in test
// files. if we are looking at one of these warnings
// from a test file we skip
if self.is_test(&source_location.file) && (*code == 1878 || *code == 5574) {
return true
}
}
self.ignored_error_codes.contains(code)
});
if !is_ignored {
writeln!(f, "\n{}", err)?;
}
} else {
writeln!(f, "\n{}", err)?;
}
}
Ok(())
}
}
/// Represents a compiled solidity contract
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
@ -1727,7 +1604,7 @@ pub struct StorageType {
pub number_of_bytes: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
#[serde(rename_all = "camelCase")]
pub struct Error {
#[serde(default, skip_serializing_if = "Option::is_none")]
@ -1757,7 +1634,7 @@ impl fmt::Display for Error {
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub enum Severity {
Error,
Warning,
@ -1840,14 +1717,14 @@ impl<'de> Deserialize<'de> for Severity {
}
}
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
pub struct SourceLocation {
pub file: String,
pub start: i32,
pub end: i32,
}
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
pub struct SecondarySourceLocation {
pub file: Option<String>,
pub start: Option<i32>,
@ -1867,7 +1744,7 @@ pub struct SourceFile {
pub struct SourceFiles(pub BTreeMap<String, SourceFile>);
impl SourceFiles {
/// Returns an iterator over the the source files' ids and path
/// Returns an iterator over the source files' ids and path
///
/// ```
/// use std::collections::BTreeMap;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,146 @@
use crate::artifacts::{CompactContractRef, Contract, FileToContractsMap};
use semver::Version;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
/// file -> [(contract name -> Contract + solc version)]
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
#[serde(transparent)]
pub struct VersionedContracts(pub FileToContractsMap<Vec<VersionedContract>>);
impl VersionedContracts {
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn len(&self) -> usize {
self.0.len()
}
/// Returns an iterator over all files
pub fn files(&self) -> impl Iterator<Item = &String> + '_ {
self.0.keys()
}
/// Finds the _first_ contract with the given name
///
/// # Example
///
/// ```
/// use ethers_solc::Project;
/// use ethers_solc::artifacts::*;
/// # fn demo(project: Project) {
/// let output = project.compile().unwrap().output();
/// let contract = output.find("Greeter").unwrap();
/// # }
/// ```
pub fn find(&self, contract: impl AsRef<str>) -> Option<CompactContractRef> {
let contract_name = contract.as_ref();
self.contracts().find_map(|(name, contract)| {
(name == contract_name).then(|| CompactContractRef::from(contract))
})
}
/// Removes the _first_ contract with the given name from the set
///
/// # Example
///
/// ```
/// use ethers_solc::Project;
/// use ethers_solc::artifacts::*;
/// # fn demo(project: Project) {
/// let (_, mut contracts) = project.compile().unwrap().output().split();
/// let contract = contracts.remove("Greeter").unwrap();
/// # }
/// ```
pub fn remove(&mut self, contract: impl AsRef<str>) -> Option<Contract> {
let contract_name = contract.as_ref();
self.0.values_mut().find_map(|all_contracts| {
let mut contract = None;
if let Some((c, mut contracts)) = all_contracts.remove_entry(contract_name) {
if !contracts.is_empty() {
contract = Some(contracts.remove(0).contract);
}
if !contracts.is_empty() {
all_contracts.insert(c, contracts);
}
}
contract
})
}
/// Given the contract file's path and the contract's name, tries to return the contract's
/// bytecode, runtime bytecode, and abi
pub fn get(&self, path: &str, contract: &str) -> Option<CompactContractRef> {
self.0
.get(path)
.and_then(|contracts| {
contracts.get(contract).and_then(|c| c.get(0).map(|c| &c.contract))
})
.map(CompactContractRef::from)
}
/// Iterate over all contracts and their names
pub fn contracts(&self) -> impl Iterator<Item = (&String, &Contract)> {
self.0
.values()
.flat_map(|c| c.iter().flat_map(|(name, c)| c.iter().map(move |c| (name, &c.contract))))
}
/// Returns an iterator over (`file`, `name`, `Contract`)
pub fn contracts_with_files(&self) -> impl Iterator<Item = (&String, &String, &Contract)> {
self.0.iter().flat_map(|(file, contracts)| {
contracts
.iter()
.flat_map(move |(name, c)| c.iter().map(move |c| (file, name, &c.contract)))
})
}
/// Returns an iterator over all contracts and their source names.
///
/// ```
/// use std::collections::BTreeMap;
/// use ethers_solc::{ artifacts::*, Artifact };
/// # fn demo(contracts: OutputContracts) {
/// let contracts: BTreeMap<String, CompactContractSome> = contracts
/// .into_contracts()
/// .map(|(k, c)| (k, c.into_compact_contract().unwrap()))
/// .collect();
/// # }
/// ```
pub fn into_contracts(self) -> impl Iterator<Item = (String, Contract)> {
self.0.into_values().flat_map(|c| {
c.into_iter()
.flat_map(|(name, c)| c.into_iter().map(move |c| (name.clone(), c.contract)))
})
}
}
impl AsRef<FileToContractsMap<Vec<VersionedContract>>> for VersionedContracts {
fn as_ref(&self) -> &FileToContractsMap<Vec<VersionedContract>> {
&self.0
}
}
impl AsMut<FileToContractsMap<Vec<VersionedContract>>> for VersionedContracts {
fn as_mut(&mut self) -> &mut FileToContractsMap<Vec<VersionedContract>> {
&mut self.0
}
}
impl IntoIterator for VersionedContracts {
type Item = (String, BTreeMap<String, Vec<VersionedContract>>);
type IntoIter =
std::collections::btree_map::IntoIter<String, BTreeMap<String, Vec<VersionedContract>>>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
/// A contract and the compiler version used to compile it
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct VersionedContract {
pub contract: Contract,
pub version: Version,
}

View File

@ -0,0 +1,40 @@
use crate::{error::Result, CompilerInput, CompilerOutput, Solc};
/// The result of a `solc` process bundled with its `Solc` and `CompilerInput`
type CompileElement = (Result<CompilerOutput>, Solc, CompilerInput);
/// The bundled output of multiple `solc` processes.
#[derive(Debug)]
pub struct CompiledMany {
outputs: Vec<CompileElement>,
}
impl CompiledMany {
pub fn new(outputs: Vec<CompileElement>) -> Self {
Self { outputs }
}
/// Returns an iterator over all output elements
pub fn outputs(&self) -> impl Iterator<Item = &CompileElement> {
self.outputs.iter()
}
/// Returns an iterator over all output elements
pub fn into_outputs(self) -> impl Iterator<Item = CompileElement> {
self.outputs.into_iter()
}
/// Returns all `CompilerOutput` or the first error that occurred
pub fn flattened(self) -> Result<Vec<CompilerOutput>> {
self.into_iter().collect()
}
}
impl IntoIterator for CompiledMany {
type Item = Result<CompilerOutput>;
type IntoIter = std::vec::IntoIter<Result<CompilerOutput>>;
fn into_iter(self) -> Self::IntoIter {
self.outputs.into_iter().map(|(res, _, _)| res).collect::<Vec<_>>().into_iter()
}
}

View File

@ -15,6 +15,11 @@ use std::{
str::FromStr,
};
pub mod contracts;
pub mod many;
pub mod output;
pub mod project;
/// The name of the `solc` binary on the system
pub const SOLC: &str = "solc";
@ -496,7 +501,7 @@ impl Solc {
.stdout(Stdio::piped())
.spawn()
.map_err(|err| SolcError::io(err, &self.solc))?;
let stdin = child.stdin.take().unwrap();
let stdin = child.stdin.take().expect("Stdin exists.");
serde_json::to_writer(stdin, input)?;
compile_output(child.wait_with_output().map_err(|err| SolcError::io(err, &self.solc))?)
}
@ -602,7 +607,7 @@ impl Solc {
/// let outputs = Solc::compile_many([(solc1, input1), (solc2, input2)], 2).await.flattened().unwrap();
/// # }
/// ```
pub async fn compile_many<I>(jobs: I, n: usize) -> CompiledMany
pub async fn compile_many<I>(jobs: I, n: usize) -> crate::many::CompiledMany
where
I: IntoIterator<Item = (Solc, CompilerInput)>,
{
@ -615,42 +620,8 @@ impl Solc {
.buffer_unordered(n)
.collect::<Vec<_>>()
.await;
CompiledMany { outputs }
}
}
/// The result of a `solc` process bundled with its `Solc` and `CompilerInput`
type CompileElement = (Result<CompilerOutput>, Solc, CompilerInput);
/// The output of multiple `solc` processes.
#[derive(Debug)]
pub struct CompiledMany {
outputs: Vec<CompileElement>,
}
impl CompiledMany {
/// Returns an iterator over all output elements
pub fn outputs(&self) -> impl Iterator<Item = &CompileElement> {
self.outputs.iter()
}
/// Returns an iterator over all output elements
pub fn into_outputs(self) -> impl Iterator<Item = CompileElement> {
self.outputs.into_iter()
}
/// Returns all `CompilerOutput` or the first error that occurred
pub fn flattened(self) -> Result<Vec<CompilerOutput>> {
self.into_iter().collect()
}
}
impl IntoIterator for CompiledMany {
type Item = Result<CompilerOutput>;
type IntoIter = std::vec::IntoIter<Result<CompilerOutput>>;
fn into_iter(self) -> Self::IntoIter {
self.outputs.into_iter().map(|(res, _, _)| res).collect::<Vec<_>>().into_iter()
crate::many::CompiledMany::new(outputs)
}
}
@ -716,7 +687,7 @@ mod tests {
#[test]
fn solc_compile_works() {
let input = include_str!("../test-data/in/compiler-in-1.json");
let input = include_str!("../../test-data/in/compiler-in-1.json");
let input: CompilerInput = serde_json::from_str(input).unwrap();
let out = solc().compile(&input).unwrap();
let other = solc().compile(&serde_json::json!(input)).unwrap();
@ -726,7 +697,7 @@ mod tests {
#[cfg(feature = "async")]
#[tokio::test]
async fn async_solc_compile_works() {
let input = include_str!("../test-data/in/compiler-in-1.json");
let input = include_str!("../../test-data/in/compiler-in-1.json");
let input: CompilerInput = serde_json::from_str(input).unwrap();
let out = solc().async_compile(&input).await.unwrap();
let other = solc().async_compile(&serde_json::json!(input)).await.unwrap();
@ -735,7 +706,7 @@ mod tests {
#[cfg(feature = "async")]
#[tokio::test]
async fn async_solc_compile_works2() {
let input = include_str!("../test-data/in/compiler-in-2.json");
let input = include_str!("../../test-data/in/compiler-in-2.json");
let input: CompilerInput = serde_json::from_str(input).unwrap();
let out = solc().async_compile(&input).await.unwrap();
let other = solc().async_compile(&serde_json::json!(input)).await.unwrap();

View File

@ -0,0 +1,363 @@
//! The output of a compiled project
use crate::{
artifacts::{CompactContractRef, Contract, Error, SourceFile, SourceFiles},
contracts::{VersionedContract, VersionedContracts},
ArtifactOutput, Artifacts, CompilerOutput,
};
use semver::Version;
use std::{collections::BTreeMap, fmt, path::Path};
/// Contains a mixture of already compiled/cached artifacts and the input set of sources that still
/// need to be compiled.
#[derive(Debug, Clone, PartialEq, Default)]
pub struct ProjectCompileOutput<T: ArtifactOutput> {
/// contains the aggregated `CompilerOutput`
///
/// See [`CompilerSources::compile`]
pub(crate) compiler_output: AggregatedCompilerOutput,
/// all artifact files from `output` that were freshly compiled and written
pub(crate) compiled_artifacts: Artifacts<T::Artifact>,
/// All artifacts that were read from cache
pub(crate) cached_artifacts: Artifacts<T::Artifact>,
/// errors that should be omitted
pub(crate) ignored_error_codes: Vec<u64>,
}
impl<T: ArtifactOutput> ProjectCompileOutput<T> {
/// All artifacts together with their contract file name and name `<file name>:<name>`
///
/// This returns a chained iterator of both cached and recompiled contract artifacts
///
/// # Example
///
/// ```no_run
/// use std::collections::btree_map::BTreeMap;
/// use ethers_solc::artifacts::CompactContractBytecode;
/// use ethers_solc::Project;
///
/// let project = Project::builder().build().unwrap();
/// let contracts: BTreeMap<String, CompactContractBytecode> = project.compile().unwrap().into_artifacts().collect();
/// ```
pub fn into_artifacts(self) -> impl Iterator<Item = (String, T::Artifact)> {
let Self { cached_artifacts, compiled_artifacts, .. } = self;
cached_artifacts.into_artifacts::<T>().chain(compiled_artifacts.into_artifacts::<T>())
}
/// All artifacts together with their contract file and name as tuple `(file, contract
/// name, artifact)`
///
/// This returns a chained iterator of both cached and recompiled contract artifacts
///
/// # Example
///
/// ```no_run
/// use std::collections::btree_map::BTreeMap;
/// use ethers_solc::artifacts::CompactContractBytecode;
/// use ethers_solc::Project;
///
/// let project = Project::builder().build().unwrap();
/// let contracts: Vec<(String, String, CompactContractBytecode)> = project.compile().unwrap().into_artifacts_with_files().collect();
/// ```
///
/// **NOTE** the `file` will be returned as is, see also [`Self::with_stripped_file_prefixes()`]
pub fn into_artifacts_with_files(self) -> impl Iterator<Item = (String, String, T::Artifact)> {
let Self { cached_artifacts, compiled_artifacts, .. } = self;
cached_artifacts
.into_artifacts_with_files()
.chain(compiled_artifacts.into_artifacts_with_files())
}
/// Strips the given prefix from all artifact file paths to make them relative to the given
/// `base` argument
///
/// # Example
///
/// Make all artifact files relative tot the project's root directory
///
/// ```no_run
/// use ethers_solc::artifacts::CompactContractBytecode;
/// use ethers_solc::Project;
///
/// let project = Project::builder().build().unwrap();
/// let output = project.compile().unwrap().with_stripped_file_prefixes(project.root());
/// ```
pub fn with_stripped_file_prefixes(mut self, base: impl AsRef<Path>) -> Self {
let base = base.as_ref();
self.cached_artifacts = self.cached_artifacts.into_stripped_file_prefixes(base);
self.compiled_artifacts = self.compiled_artifacts.into_stripped_file_prefixes(base);
self
}
/// Get the (merged) solc compiler output
/// ```no_run
/// use std::collections::btree_map::BTreeMap;
/// use ethers_solc::artifacts::Contract;
/// use ethers_solc::Project;
///
/// let project = Project::builder().build().unwrap();
/// let contracts: BTreeMap<String, Contract> =
/// project.compile().unwrap().output().contracts_into_iter().collect();
/// ```
pub fn output(self) -> AggregatedCompilerOutput {
self.compiler_output
}
/// Whether this type has a compiler output
pub fn has_compiled_contracts(&self) -> bool {
self.compiler_output.is_empty()
}
/// Whether this type does not contain compiled contracts
pub fn is_unchanged(&self) -> bool {
self.compiler_output.is_unchanged()
}
/// Whether there were errors
pub fn has_compiler_errors(&self) -> bool {
self.compiler_output.has_error()
}
/// Whether there were warnings
pub fn has_compiler_warnings(&self) -> bool {
self.compiler_output.has_warning(&self.ignored_error_codes)
}
/// Finds the first contract with the given name and removes it from the set
pub fn remove(&mut self, contract_name: impl AsRef<str>) -> Option<T::Artifact> {
let contract_name = contract_name.as_ref();
if let artifact @ Some(_) = self.compiled_artifacts.remove(contract_name) {
return artifact
}
self.cached_artifacts.remove(contract_name)
}
/// Returns the set of `Artifacts` that were cached and got reused during [`Project::compile()`]
pub fn cached_artifacts(&self) -> &Artifacts<T::Artifact> {
&self.cached_artifacts
}
/// Returns the set of `Artifacts` that were compiled with `solc` in [`Project::compile()`]
pub fn compiled_artifacts(&self) -> &Artifacts<T::Artifact> {
&self.compiled_artifacts
}
}
impl<T: ArtifactOutput> ProjectCompileOutput<T>
where
T::Artifact: Clone,
{
/// Finds the first contract with the given name
pub fn find(&self, contract_name: impl AsRef<str>) -> Option<&T::Artifact> {
let contract_name = contract_name.as_ref();
if let artifact @ Some(_) = self.compiled_artifacts.find(contract_name) {
return artifact
}
self.cached_artifacts.find(contract_name)
}
}
impl<T: ArtifactOutput> fmt::Display for ProjectCompileOutput<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.compiler_output.is_unchanged() {
f.write_str("Nothing to compile")
} else {
self.compiler_output.diagnostics(&self.ignored_error_codes).fmt(f)
}
}
}
/// The aggregated output of (multiple) compile jobs
///
/// This is effectively a solc version aware `CompilerOutput`
#[derive(Clone, Debug, Default, PartialEq)]
pub struct AggregatedCompilerOutput {
/// all errors from all `CompilerOutput`
pub errors: Vec<Error>,
/// All source files
pub sources: BTreeMap<String, SourceFile>,
/// All compiled contracts combined with the solc version used to compile them
pub contracts: VersionedContracts,
}
impl AggregatedCompilerOutput {
/// Whether the output contains a compiler error
pub fn has_error(&self) -> bool {
self.errors.iter().any(|err| err.severity.is_error())
}
/// Whether the output contains a compiler warning
pub fn has_warning(&self, ignored_error_codes: &[u64]) -> bool {
self.errors.iter().any(|err| {
if err.severity.is_warning() {
err.error_code.as_ref().map_or(false, |code| !ignored_error_codes.contains(code))
} else {
false
}
})
}
pub fn diagnostics<'a>(&'a self, ignored_error_codes: &'a [u64]) -> OutputDiagnostics {
OutputDiagnostics { compiler_output: self, ignored_error_codes }
}
pub fn is_empty(&self) -> bool {
self.contracts.is_empty()
}
pub fn is_unchanged(&self) -> bool {
self.contracts.is_empty() && self.errors.is_empty()
}
pub fn extend_all<I>(&mut self, out: I)
where
I: IntoIterator<Item = (Version, CompilerOutput)>,
{
for (v, o) in out {
self.extend(v, o)
}
}
/// adds a new `CompilerOutput` to the aggregated output
pub fn extend(&mut self, version: Version, output: CompilerOutput) {
self.errors.extend(output.errors);
self.sources.extend(output.sources);
for (file_name, new_contracts) in output.contracts {
let contracts = self.contracts.as_mut().entry(file_name).or_default();
for (contract_name, contract) in new_contracts {
let versioned = contracts.entry(contract_name).or_default();
versioned.push(VersionedContract { contract, version: version.clone() });
}
}
}
/// Finds the _first_ contract with the given name
///
/// # Example
///
/// ```
/// use ethers_solc::Project;
/// use ethers_solc::artifacts::*;
/// # fn demo(project: Project) {
/// let output = project.compile().unwrap().output();
/// let contract = output.find("Greeter").unwrap();
/// # }
/// ```
pub fn find(&self, contract: impl AsRef<str>) -> Option<CompactContractRef> {
self.contracts.find(contract)
}
/// Removes the _first_ contract with the given name from the set
///
/// # Example
///
/// ```
/// use ethers_solc::Project;
/// use ethers_solc::artifacts::*;
/// # fn demo(project: Project) {
/// let mut output = project.compile().unwrap().output();
/// let contract = output.remove("Greeter").unwrap();
/// # }
/// ```
pub fn remove(&mut self, contract: impl AsRef<str>) -> Option<Contract> {
self.contracts.remove(contract)
}
/// Iterate over all contracts and their names
pub fn contracts_iter(&self) -> impl Iterator<Item = (&String, &Contract)> {
self.contracts.contracts()
}
/// Iterate over all contracts and their names
pub fn contracts_into_iter(self) -> impl Iterator<Item = (String, Contract)> {
self.contracts.into_contracts()
}
/// Given the contract file's path and the contract's name, tries to return the contract's
/// bytecode, runtime bytecode, and abi
pub fn get(&self, path: &str, contract: &str) -> Option<CompactContractRef> {
self.contracts.get(path, contract)
}
/// Returns the output's source files and contracts separately, wrapped in helper types that
/// provide several helper methods
///
/// # Example
///
/// ```
/// use ethers_solc::Project;
/// # fn demo(project: Project) {
/// let output = project.compile().unwrap().output();
/// let (sources, contracts) = output.split();
/// # }
/// ```
pub fn split(self) -> (SourceFiles, VersionedContracts) {
(SourceFiles(self.sources), self.contracts)
}
}
/// Helper type to implement display for solc errors
#[derive(Clone, Debug)]
pub struct OutputDiagnostics<'a> {
compiler_output: &'a AggregatedCompilerOutput,
ignored_error_codes: &'a [u64],
}
impl<'a> OutputDiagnostics<'a> {
/// Returns true if there is at least one error of high severity
pub fn has_error(&self) -> bool {
self.compiler_output.has_error()
}
/// Returns true if there is at least one warning
pub fn has_warning(&self) -> bool {
self.compiler_output.has_warning(self.ignored_error_codes)
}
/// Returns true if the contract is a expected to be a test
fn is_test<T: AsRef<str>>(&self, contract_path: T) -> bool {
if contract_path.as_ref().ends_with(".t.sol") {
return true
}
self.compiler_output.find(&contract_path).map_or(false, |contract| {
contract.abi.map_or(false, |abi| abi.functions.contains_key("IS_TEST"))
})
}
}
impl<'a> fmt::Display for OutputDiagnostics<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.has_error() {
f.write_str("Compiler run failed")?;
} else if self.has_warning() {
f.write_str("Compiler run successful (with warnings)")?;
} else {
f.write_str("Compiler run successful")?;
}
for err in &self.compiler_output.errors {
if err.severity.is_warning() {
let is_ignored = err.error_code.as_ref().map_or(false, |code| {
if let Some(source_location) = &err.source_location {
// we ignore spdx and contract size warnings in test
// files. if we are looking at one of these warnings
// from a test file we skip
if self.is_test(&source_location.file) && (*code == 1878 || *code == 5574) {
return true
}
}
self.ignored_error_codes.contains(code)
});
if !is_ignored {
writeln!(f, "\n{}", err)?;
}
} else {
writeln!(f, "\n{}", err)?;
}
}
Ok(())
}
}

View File

@ -0,0 +1,440 @@
//! Manages compiling of a `Project`
//!
//! The compilation of a project is performed in several steps.
//!
//! First the project's dependency graph [`crate::Graph`] is constructed and all imported
//! dependencies are resolved. The graph holds all the relationships between the files and their
//! versions. From there the appropriate version set is derived
//! [`crate::Graph::into_sources_by_version()`] which need to be compiled with different
//! [`crate::Solc`] versions.
//!
//! At this point we check if we need to compile a source file or whether we can reuse an _existing_
//! `Artifact`. We don't to compile if:
//! - caching is enabled
//! - the file is **not** dirty [`Cache::is_dirty()`]
//! - the artifact for that file exists
//!
//! This concludes the preprocessing, and we now have either
//! - only `Source` files that need to be compiled
//! - only cached `Artifacts`, compilation can be skipped. This is considered an unchanged,
//! cached project
//! - Mix of both `Source` and `Artifacts`, only the `Source` files need to be compiled, the
//! `Artifacts` can be reused.
//!
//! The final step is invoking `Solc` via the standard JSON format.
//!
//! ### Notes on [Import Path Resolution](https://docs.soliditylang.org/en/develop/path-resolution.html#path-resolution)
//!
//! In order to be able to support reproducible builds on all platforms, the Solidity compiler has
//! to abstract away the details of the filesystem where source files are stored. Paths used in
//! imports must work the same way everywhere while the command-line interface must be able to work
//! with platform-specific paths to provide good user experience. This section aims to explain in
//! detail how Solidity reconciles these requirements.
//!
//! The compiler maintains an internal database (virtual filesystem or VFS for short) where each
//! source unit is assigned a unique source unit name which is an opaque and unstructured
//! identifier. When you use the import statement, you specify an import path that references a
//! source unit name. If the compiler does not find any source unit name matching the import path in
//! the VFS, it invokes the callback, which is responsible for obtaining the source code to be
//! placed under that name.
//!
//! This becomes relevant when dealing with resolved imports
//!
//! #### Relative Imports
//!
//! ```solidity
//! import "./math/math.sol";
//! import "contracts/tokens/token.sol";
//! ```
//! In the above `./math/math.sol` and `contracts/tokens/token.sol` are import paths while the
//! source unit names they translate to are `contracts/math/math.sol` and
//! `contracts/tokens/token.sol` respectively.
//!
//! #### Direct Imports
//!
//! An import that does not start with `./` or `../` is a direct import.
//!
//! ```solidity
//! import "/project/lib/util.sol"; // source unit name: /project/lib/util.sol
//! import "lib/util.sol"; // source unit name: lib/util.sol
//! import "@openzeppelin/address.sol"; // source unit name: @openzeppelin/address.sol
//! import "https://example.com/token.sol"; // source unit name: https://example.com/token.sol
//! ```
//!
//! After applying any import remappings the import path simply becomes the source unit name.
//!
//! ##### Import Remapping
//!
//! ```solidity
//! import "github.com/ethereum/dapp-bin/library/math.sol"; // source unit name: dapp-bin/library/math.sol
//! ```
//!
//! If compiled with `solc github.com/ethereum/dapp-bin/=dapp-bin/` the compiler will look for the
//! file in the VFS under `dapp-bin/library/math.sol`. If the file is not available there, the
//! source unit name will be passed to the Host Filesystem Loader, which will then look in
//! `/project/dapp-bin/library/iterable_mapping.sol`
use crate::{
artifact_output::Artifacts,
artifacts::{Settings, VersionedSources},
cache::ArtifactsCache,
error::Result,
output::AggregatedCompilerOutput,
resolver::GraphEdges,
ArtifactOutput, CompilerInput, Graph, Project, ProjectCompileOutput, ProjectPathsConfig, Solc,
Sources,
};
use rayon::prelude::*;
use std::collections::btree_map::BTreeMap;
#[derive(Debug)]
pub struct ProjectCompiler<'a, T: ArtifactOutput> {
/// Contains the relationship of the source files and their imports
edges: GraphEdges,
project: &'a Project<T>,
/// how to compile all the sources
sources: CompilerSources,
}
impl<'a, T: ArtifactOutput> ProjectCompiler<'a, T> {
/// Create a new `ProjectCompiler` to bootstrap the compilation process of the project's
/// sources.
///
/// # Example
///
/// ```no_run
/// use ethers_solc::Project;
///
/// let project = Project::builder().build().unwrap();
/// let output = project.compile().unwrap();
/// ```
#[cfg(all(feature = "svm", feature = "async"))]
pub fn new(project: &'a Project<T>) -> Result<Self> {
Self::with_sources(project, project.paths.read_input_files()?)
}
/// Bootstraps the compilation process by resolving the dependency graph of all sources and the
/// appropriate `Solc` -> `Sources` set as well as the compile mode to use (parallel,
/// sequential)
///
/// Multiple (`Solc` -> `Sources`) pairs can be compiled in parallel if the `Project` allows
/// multiple `jobs`, see [`crate::Project::set_solc_jobs()`].
#[cfg(all(feature = "svm", feature = "async"))]
pub fn with_sources(project: &'a Project<T>, sources: Sources) -> Result<Self> {
let graph = Graph::resolve_sources(&project.paths, sources)?;
let (versions, edges) = graph.into_sources_by_version(project.offline)?;
let sources_by_version = versions.get(&project.allowed_lib_paths)?;
let sources = if project.solc_jobs > 1 && sources_by_version.len() > 1 {
// if there are multiple different versions, and we can use multiple jobs we can compile
// them in parallel
CompilerSources::Parallel(sources_by_version, project.solc_jobs)
} else {
CompilerSources::Sequential(sources_by_version)
};
Ok(Self { edges, project, sources })
}
/// Compiles the sources with a pinned `Solc` instance
pub fn with_sources_and_solc(
project: &'a Project<T>,
sources: Sources,
solc: Solc,
) -> Result<Self> {
let version = solc.version()?;
let (sources, edges) = Graph::resolve_sources(&project.paths, sources)?.into_sources();
let sources_by_version = BTreeMap::from([(solc, (version, sources))]);
let sources = CompilerSources::Sequential(sources_by_version);
Ok(Self { edges, project, sources })
}
/// Compiles all the sources of the `Project` in the appropriate mode
///
/// If caching is enabled, the sources are filtered and only _dirty_ sources are recompiled.
///
/// The output of the compile process can be a mix of reused artifacts and freshly compiled
/// `Contract`s
///
/// # Example
///
/// ```no_run
/// use ethers_solc::Project;
///
/// let project = Project::builder().build().unwrap();
/// let output = project.compile().unwrap();
/// ```
pub fn compile(self) -> Result<ProjectCompileOutput<T>> {
// drive the compiler statemachine to completion
self.preprocess()?.compile()?.write_artifacts()?.write_cache()
}
/// Does basic preprocessing
/// - sets proper source unit names
/// - check cache
fn preprocess(self) -> Result<PreprocessedState<'a, T>> {
let Self { edges, project, mut sources } = self;
let mut cache = ArtifactsCache::new(project, edges)?;
// retain and compile only dirty sources
sources = sources.filtered(&mut cache);
Ok(PreprocessedState { sources, cache })
}
}
/// A series of states that comprise the [`ProjectCompiler::compile()`] state machine
///
/// The main reason is to debug all states individually
#[derive(Debug)]
struct PreprocessedState<'a, T: ArtifactOutput> {
sources: CompilerSources,
cache: ArtifactsCache<'a, T>,
}
impl<'a, T: ArtifactOutput> PreprocessedState<'a, T> {
/// advance to the next state by compiling all sources
fn compile(self) -> Result<CompiledState<'a, T>> {
let PreprocessedState { sources, cache } = self;
let output =
sources.compile(&cache.project().solc_config.settings, &cache.project().paths)?;
Ok(CompiledState { output, cache })
}
}
/// Represents the state after `solc` was successfully invoked
#[derive(Debug)]
struct CompiledState<'a, T: ArtifactOutput> {
output: AggregatedCompilerOutput,
cache: ArtifactsCache<'a, T>,
}
impl<'a, T: ArtifactOutput> CompiledState<'a, T> {
/// advance to the next state by handling all artifacts
///
/// Writes all output contracts to disk if enabled in the `Project`
fn write_artifacts(self) -> Result<ArtifactsState<'a, T>> {
let CompiledState { output, cache } = self;
// write all artifacts
let compiled_artifacts = if !cache.project().no_artifacts {
T::on_output(&output.contracts, &cache.project().paths)?
} else {
T::output_to_artifacts(&output.contracts)
};
Ok(ArtifactsState { output, cache, compiled_artifacts })
}
}
/// Represents the state after all artifacts were written to disk
#[derive(Debug)]
struct ArtifactsState<'a, T: ArtifactOutput> {
output: AggregatedCompilerOutput,
cache: ArtifactsCache<'a, T>,
compiled_artifacts: Artifacts<T::Artifact>,
}
impl<'a, T: ArtifactOutput> ArtifactsState<'a, T> {
/// Writes the cache file
///
/// this concludes the [`Project::compile()`] statemachine
fn write_cache(self) -> Result<ProjectCompileOutput<T>> {
let ArtifactsState { output, cache, compiled_artifacts } = self;
let ignored_error_codes = cache.project().ignored_error_codes.clone();
let cached_artifacts = cache.write_cache(&compiled_artifacts)?;
Ok(ProjectCompileOutput {
compiler_output: output,
compiled_artifacts,
cached_artifacts,
ignored_error_codes,
})
}
}
/// Determines how the `solc <-> sources` pairs are executed
#[derive(Debug, Clone)]
#[allow(dead_code)]
enum CompilerSources {
/// Compile all these sequentially
Sequential(VersionedSources),
/// Compile all these in parallel using a certain amount of jobs
Parallel(VersionedSources, usize),
}
impl CompilerSources {
/// Filters out all sources that don't need to be compiled, see [`ArtifactsCache::filter`]
fn filtered<T: ArtifactOutput>(self, cache: &mut ArtifactsCache<T>) -> Self {
fn filtered_sources<T: ArtifactOutput>(
sources: VersionedSources,
cache: &mut ArtifactsCache<T>,
) -> VersionedSources {
sources
.into_iter()
.map(|(solc, (version, sources))| {
let sources = cache.filter(sources, &version);
(solc, (version, sources))
})
.collect()
}
match self {
CompilerSources::Sequential(s) => {
CompilerSources::Sequential(filtered_sources(s, cache))
}
CompilerSources::Parallel(s, j) => {
CompilerSources::Parallel(filtered_sources(s, cache), j)
}
}
}
/// Compiles all the files with `Solc`
fn compile(
self,
settings: &Settings,
paths: &ProjectPathsConfig,
) -> Result<AggregatedCompilerOutput> {
match self {
CompilerSources::Sequential(input) => compile_sequential(input, settings, paths),
CompilerSources::Parallel(input, j) => compile_parallel(input, j, settings, paths),
}
}
}
/// Compiles the input set sequentially and returns an aggregated set of the solc `CompilerOutput`s
fn compile_sequential(
input: VersionedSources,
settings: &Settings,
paths: &ProjectPathsConfig,
) -> Result<AggregatedCompilerOutput> {
let mut aggregated = AggregatedCompilerOutput::default();
tracing::trace!("compiling {} jobs sequentially", input.len());
for (solc, (version, sources)) in input {
if sources.is_empty() {
// nothing to compile
continue
}
tracing::trace!(
"compiling {} sources with solc \"{}\" {:?}",
sources.len(),
solc.as_ref().display(),
solc.args
);
let input = CompilerInput::with_sources(sources)
.settings(settings.clone())
.normalize_evm_version(&version)
.with_remappings(paths.remappings.clone());
tracing::trace!(
"calling solc `{}` with {} sources {:?}",
version,
input.sources.len(),
input.sources.keys()
);
let output = solc.compile(&input)?;
tracing::trace!("compiled input, output has error: {}", output.has_error());
aggregated.extend(version, output);
}
Ok(aggregated)
}
/// compiles the input set using `num_jobs` threads
fn compile_parallel(
input: VersionedSources,
num_jobs: usize,
settings: &Settings,
paths: &ProjectPathsConfig,
) -> Result<AggregatedCompilerOutput> {
debug_assert!(num_jobs > 1);
tracing::trace!("compile sources in parallel using up to {} solc jobs", num_jobs);
let mut jobs = Vec::with_capacity(input.len());
for (solc, (version, sources)) in input {
if sources.is_empty() {
// nothing to compile
continue
}
let job = CompilerInput::with_sources(sources)
.settings(settings.clone())
.normalize_evm_version(&version)
.with_remappings(paths.remappings.clone());
jobs.push((solc, version, job))
}
// start a rayon threadpool that will execute all `Solc::compile()` processes
let pool = rayon::ThreadPoolBuilder::new().num_threads(num_jobs).build().unwrap();
let outputs = pool.install(move || {
jobs.into_par_iter()
.map(|(solc, version, input)| {
tracing::trace!(
"calling solc `{}` {:?} with {} sources: {:?}",
version,
solc.args,
input.sources.len(),
input.sources.keys()
);
solc.compile(&input).map(|output| (version, output))
})
.collect::<Result<Vec<_>>>()
})?;
let mut aggregated = AggregatedCompilerOutput::default();
aggregated.extend_all(outputs);
Ok(aggregated)
}
#[cfg(test)]
#[cfg(feature = "project-util")]
mod tests {
use super::*;
use crate::{project_util::TempProject, MinimalCombinedArtifacts};
use std::path::PathBuf;
#[allow(unused)]
fn init_tracing() {
let _ = tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.try_init()
.ok();
}
#[test]
fn can_preprocess() {
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test-data/dapp-sample");
let project =
Project::builder().paths(ProjectPathsConfig::dapptools(root).unwrap()).build().unwrap();
let compiler = ProjectCompiler::new(&project).unwrap();
let prep = compiler.preprocess().unwrap();
let cache = prep.cache.as_cached().unwrap();
// 3 contracts
assert_eq!(cache.dirty_entries.len(), 3);
assert!(cache.filtered.is_empty());
assert!(cache.cache.is_empty());
let compiled = prep.compile().unwrap();
assert_eq!(compiled.output.contracts.files().count(), 3);
}
#[test]
fn can_detect_cached_files() {
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test-data/dapp-sample");
let paths = ProjectPathsConfig::builder().sources(root.join("src")).lib(root.join("lib"));
let project = TempProject::<MinimalCombinedArtifacts>::new(paths).unwrap();
let compiled = project.compile().unwrap();
assert!(!compiled.has_compiler_errors());
let inner = project.project();
let compiler = ProjectCompiler::new(inner).unwrap();
let prep = compiler.preprocess().unwrap();
assert!(prep.cache.as_cached().unwrap().dirty_entries.is_empty())
}
}

View File

@ -1,19 +1,16 @@
use crate::{
artifacts::{CompactContract, CompactContractBytecode, Contract, Settings},
artifacts::Settings,
cache::SOLIDITY_FILES_CACHE_FILENAME,
error::{Result, SolcError, SolcIoError},
hh::HardhatArtifact,
remappings::Remapping,
resolver::Graph,
utils, CompilerOutput, Source, Sources,
utils, Source, Sources,
};
use ethers_core::{abi::Abi, types::Bytes};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde::{Deserialize, Serialize};
use std::{
collections::BTreeMap,
convert::TryFrom,
fmt::{self, Formatter},
fs, io,
fs,
path::{Component, Path, PathBuf},
};
@ -358,27 +355,27 @@ pub struct ProjectPathsConfigBuilder {
impl ProjectPathsConfigBuilder {
pub fn root(mut self, root: impl Into<PathBuf>) -> Self {
self.root = Some(canonicalized(root));
self.root = Some(utils::canonicalized(root));
self
}
pub fn cache(mut self, cache: impl Into<PathBuf>) -> Self {
self.cache = Some(canonicalized(cache));
self.cache = Some(utils::canonicalized(cache));
self
}
pub fn artifacts(mut self, artifacts: impl Into<PathBuf>) -> Self {
self.artifacts = Some(canonicalized(artifacts));
self.artifacts = Some(utils::canonicalized(artifacts));
self
}
pub fn sources(mut self, sources: impl Into<PathBuf>) -> Self {
self.sources = Some(canonicalized(sources));
self.sources = Some(utils::canonicalized(sources));
self
}
pub fn tests(mut self, tests: impl Into<PathBuf>) -> Self {
self.tests = Some(canonicalized(tests));
self.tests = Some(utils::canonicalized(tests));
self
}
@ -389,14 +386,14 @@ impl ProjectPathsConfigBuilder {
}
pub fn lib(mut self, lib: impl Into<PathBuf>) -> Self {
self.libraries.get_or_insert_with(Vec::new).push(canonicalized(lib));
self.libraries.get_or_insert_with(Vec::new).push(utils::canonicalized(lib));
self
}
pub fn libs(mut self, libs: impl IntoIterator<Item = impl Into<PathBuf>>) -> Self {
let libraries = self.libraries.get_or_insert_with(Vec::new);
for lib in libs.into_iter() {
libraries.push(canonicalized(lib));
libraries.push(utils::canonicalized(lib));
}
self
}
@ -415,7 +412,10 @@ impl ProjectPathsConfigBuilder {
}
pub fn build_with_root(self, root: impl Into<PathBuf>) -> ProjectPathsConfig {
let root = canonicalized(root);
let root = utils::canonicalized(root);
let libraries = self.libraries.unwrap_or_else(|| ProjectPathsConfig::find_libs(&root));
ProjectPathsConfig {
cache: self
.cache
@ -425,8 +425,10 @@ impl ProjectPathsConfigBuilder {
.unwrap_or_else(|| ProjectPathsConfig::find_artifacts_dir(&root)),
sources: self.sources.unwrap_or_else(|| ProjectPathsConfig::find_source_dir(&root)),
tests: self.tests.unwrap_or_else(|| root.join("tests")),
libraries: self.libraries.unwrap_or_else(|| ProjectPathsConfig::find_libs(&root)),
remappings: self.remappings.unwrap_or_default(),
remappings: self
.remappings
.unwrap_or_else(|| libraries.iter().flat_map(Remapping::find_many).collect()),
libraries,
root,
}
}
@ -442,20 +444,6 @@ impl ProjectPathsConfigBuilder {
}
}
/// Returns the same path config but with canonicalized paths.
///
/// This will take care of potential symbolic linked directories.
/// For example, the tempdir library is creating directories hosted under `/var/`, which in OS X
/// is a symbolic link to `/private/var/`. So if when we try to resolve imports and a path is
/// rooted in a symbolic directory we might end up with different paths for the same file, like
/// `private/var/.../Dapp.sol` and `/var/.../Dapp.sol`
///
/// This canonicalizes all the paths but does not treat non existing dirs as an error
fn canonicalized(path: impl Into<PathBuf>) -> PathBuf {
let path = path.into();
utils::canonicalize(&path).unwrap_or(path)
}
/// The config to use when compiling the contracts
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct SolcConfig {
@ -497,229 +485,6 @@ impl SolcConfigBuilder {
}
}
pub type Artifacts<T> = BTreeMap<String, BTreeMap<String, T>>;
pub trait Artifact {
/// Returns the artifact's `Abi` and bytecode
fn into_inner(self) -> (Option<Abi>, Option<Bytes>);
/// Turns the artifact into a container type for abi, compact bytecode and deployed bytecode
fn into_compact_contract(self) -> CompactContract;
/// Turns the artifact into a container type for abi, full bytecode and deployed bytecode
fn into_contract_bytecode(self) -> CompactContractBytecode;
/// Returns the contents of this type as a single tuple of abi, bytecode and deployed bytecode
fn into_parts(self) -> (Option<Abi>, Option<Bytes>, Option<Bytes>);
}
impl<T> Artifact for T
where
T: Into<CompactContractBytecode> + Into<CompactContract>,
{
fn into_inner(self) -> (Option<Abi>, Option<Bytes>) {
let artifact = self.into_compact_contract();
(artifact.abi, artifact.bin.and_then(|bin| bin.into_bytes()))
}
fn into_compact_contract(self) -> CompactContract {
self.into()
}
fn into_contract_bytecode(self) -> CompactContractBytecode {
self.into()
}
fn into_parts(self) -> (Option<Abi>, Option<Bytes>, Option<Bytes>) {
self.into_compact_contract().into_parts()
}
}
pub trait ArtifactOutput {
/// How Artifacts are stored
type Artifact: Artifact + DeserializeOwned;
/// 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<str>) -> 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<Path>, name: impl AsRef<str>) -> 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<Path>) -> Option<String> {
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<Path>,
name: impl AsRef<str>,
root: impl AsRef<Path>,
) -> bool {
root.as_ref().join(Self::output_file(contract_file, name)).exists()
}
fn read_cached_artifact(path: impl AsRef<Path>) -> Result<Self::Artifact> {
let path = path.as_ref();
let file = fs::File::open(path).map_err(|err| SolcError::io(err, path))?;
let file = io::BufReader::new(file);
Ok(serde_json::from_reader(file)?)
}
/// Read the cached artifacts from disk
fn read_cached_artifacts<T, I>(files: I) -> Result<BTreeMap<PathBuf, Self::Artifact>>
where
I: IntoIterator<Item = T>,
T: Into<PathBuf>,
{
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(file: &str, name: &str, contract: Contract) -> Self::Artifact;
/// Convert the compiler output into a set of artifacts
fn output_to_artifacts(output: CompilerOutput) -> Artifacts<Self::Artifact> {
output
.contracts
.into_iter()
.map(|(file, contracts)| {
let contracts = contracts
.into_iter()
.map(|(name, c)| {
let contract = Self::contract_to_artifact(&file, &name, c);
(name, contract)
})
.collect();
(file, contracts)
})
.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 = CompactContractBytecode;
fn on_output(output: &CompilerOutput, layout: &ProjectPathsConfig) -> Result<()> {
fs::create_dir_all(&layout.artifacts)
.map_err(|err| SolcError::msg(format!("Failed to create artifacts dir: {}", err)))?;
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).map_err(|err| {
SolcError::msg(format!(
"Failed to create artifact parent folder \"{}\": {}",
parent.display(),
err
))
})?;
}
if let Some(iropt) = &contract.ir_optimized {
fs::write(&file.with_extension("iropt"), iropt)
.map_err(|err| SolcError::io(err, file.with_extension("iropt")))?
}
if let Some(ir) = &contract.ir {
fs::write(&file.with_extension("ir"), ir)
.map_err(|err| SolcError::io(err, file.with_extension("ir")))?
}
if let Some(ewasm) = &contract.ewasm {
fs::write(&file.with_extension("ewasm"), serde_json::to_vec_pretty(&ewasm)?)
.map_err(|err| SolcError::io(err, file.with_extension("ewasm")))?;
}
if let Some(evm) = &contract.evm {
if let Some(asm) = &evm.assembly {
fs::write(&file.with_extension("asm"), asm)
.map_err(|err| SolcError::io(err, file.with_extension("asm")))?
}
}
let min = CompactContractBytecode::from(contract.clone());
fs::write(&file, serde_json::to_vec_pretty(&min)?)
.map_err(|err| SolcError::io(err, file))?
}
}
Ok(())
}
fn contract_to_artifact(_file: &str, _name: &str, contract: Contract) -> Self::Artifact {
Self::Artifact::from(contract)
}
}
/// An Artifacts handler implementation that works the same as `MinimalCombinedArtifacts` but also
/// supports reading hardhat artifacts if an initial attempt to deserialize an artifact failed
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub struct MinimalCombinedArtifactsHardhatFallback;
impl ArtifactOutput for MinimalCombinedArtifactsHardhatFallback {
type Artifact = CompactContractBytecode;
fn on_output(output: &CompilerOutput, layout: &ProjectPathsConfig) -> Result<()> {
MinimalCombinedArtifacts::on_output(output, layout)
}
fn read_cached_artifact(path: impl AsRef<Path>) -> Result<Self::Artifact> {
let path = path.as_ref();
let content = fs::read_to_string(path).map_err(|err| SolcError::io(err, path))?;
if let Ok(a) = serde_json::from_str(&content) {
Ok(a)
} else {
tracing::error!("Failed to deserialize compact artifact");
tracing::trace!("Fallback to hardhat artifact deserialization");
let artifact = serde_json::from_str::<HardhatArtifact>(&content)?;
tracing::trace!("successfully deserialized hardhat artifact");
Ok(artifact.into_contract_bytecode())
}
}
fn contract_to_artifact(file: &str, name: &str, contract: Contract) -> Self::Artifact {
MinimalCombinedArtifacts::contract_to_artifact(file, name, contract)
}
}
/// Helper struct for serializing `--allow-paths` arguments to Solc
///
/// From the [Solc docs](https://docs.soliditylang.org/en/v0.8.9/using-the-compiler.html#base-path-and-import-remapping):
@ -751,19 +516,10 @@ impl fmt::Display for AllowedLibPaths {
}
}
impl<T: Into<PathBuf>> TryFrom<Vec<T>> for AllowedLibPaths {
type Error = SolcIoError;
fn try_from(libs: Vec<T>) -> std::result::Result<Self, Self::Error> {
let libs = libs
.into_iter()
.map(|lib| {
let path: PathBuf = lib.into();
let lib = utils::canonicalize(&path)?;
Ok(lib)
})
.collect::<std::result::Result<Vec<_>, _>>()?;
Ok(AllowedLibPaths(libs))
impl<T: Into<PathBuf>> From<Vec<T>> for AllowedLibPaths {
fn from(libs: Vec<T>) -> Self {
let libs = libs.into_iter().map(utils::canonicalized).collect();
AllowedLibPaths(libs)
}
}
@ -787,13 +543,13 @@ mod tests {
assert_eq!(ProjectPathsConfig::find_source_dir(root), contracts,);
assert_eq!(
ProjectPathsConfig::builder().build_with_root(&root).sources,
canonicalized(contracts),
utils::canonicalized(contracts),
);
std::fs::File::create(&src).unwrap();
assert_eq!(ProjectPathsConfig::find_source_dir(root), src,);
assert_eq!(
ProjectPathsConfig::builder().build_with_root(&root).sources,
canonicalized(src),
utils::canonicalized(src),
);
assert_eq!(ProjectPathsConfig::find_artifacts_dir(root), out,);
@ -801,13 +557,13 @@ mod tests {
assert_eq!(ProjectPathsConfig::find_artifacts_dir(root), artifacts,);
assert_eq!(
ProjectPathsConfig::builder().build_with_root(&root).artifacts,
canonicalized(artifacts),
utils::canonicalized(artifacts),
);
std::fs::File::create(&out).unwrap();
assert_eq!(ProjectPathsConfig::find_artifacts_dir(root), out,);
assert_eq!(
ProjectPathsConfig::builder().build_with_root(&root).artifacts,
canonicalized(out),
utils::canonicalized(out),
);
assert_eq!(ProjectPathsConfig::find_libs(root), vec![lib.clone()],);
@ -815,13 +571,13 @@ mod tests {
assert_eq!(ProjectPathsConfig::find_libs(root), vec![node_modules.clone()],);
assert_eq!(
ProjectPathsConfig::builder().build_with_root(&root).libraries,
vec![canonicalized(node_modules)],
vec![utils::canonicalized(node_modules)],
);
std::fs::File::create(&lib).unwrap();
assert_eq!(ProjectPathsConfig::find_libs(root), vec![lib.clone()],);
assert_eq!(
ProjectPathsConfig::builder().build_with_root(&root).libraries,
vec![canonicalized(lib)],
vec![utils::canonicalized(lib)],
);
}
}

View File

@ -34,6 +34,9 @@ pub enum SolcError {
#[error("{0}")]
Message(String),
#[error("No artifact found for `{}:{}`", .0.display(), .1)]
ArtifactNotFound(PathBuf, String),
#[cfg(feature = "project-util")]
#[error(transparent)]
FsExtra(#[from] fs_extra::error::Error),

View File

@ -5,12 +5,11 @@ use crate::{
Bytecode, BytecodeObject, CompactContract, CompactContractBytecode, Contract,
ContractBytecode, DeployedBytecode, Offsets,
},
error::{Result, SolcError},
ArtifactOutput, CompilerOutput, ProjectPathsConfig,
ArtifactOutput,
};
use ethers_core::abi::Abi;
use serde::{Deserialize, Serialize};
use std::{collections::BTreeMap, fs};
use std::collections::btree_map::BTreeMap;
const HH_ARTIFACT_VERSION: &str = "hh-sol-artifact-1";
@ -85,30 +84,6 @@ pub struct HardhatArtifacts;
impl ArtifactOutput for HardhatArtifacts {
type Artifact = HardhatArtifact;
fn on_output(output: &CompilerOutput, layout: &ProjectPathsConfig) -> Result<()> {
fs::create_dir_all(&layout.artifacts)
.map_err(|err| SolcError::msg(format!("Failed to create artifacts dir: {}", err)))?;
for (file, contracts) in output.contracts.iter() {
for (name, contract) in contracts {
let artifact = Self::output_file(file, name);
let artifact_file = layout.artifacts.join(artifact);
if let Some(parent) = artifact_file.parent() {
fs::create_dir_all(parent).map_err(|err| {
SolcError::msg(format!(
"Failed to create artifact parent folder \"{}\": {}",
parent.display(),
err
))
})?;
}
let artifact = Self::contract_to_artifact(file, name, contract.clone());
fs::write(&artifact_file, serde_json::to_vec_pretty(&artifact)?)
.map_err(|err| SolcError::io(err, artifact_file))?
}
}
Ok(())
}
fn contract_to_artifact(file: &str, name: &str, contract: Contract) -> Self::Artifact {
let (bytecode, link_references, deployed_bytecode, deployed_link_references) =
if let Some(evm) = contract.evm {

View File

@ -1,46 +1,38 @@
#![doc = include_str ! ("../README.md")]
pub mod artifacts;
pub mod sourcemap;
pub use artifacts::{CompilerInput, CompilerOutput, EvmVersion};
use std::collections::btree_map::Entry;
mod artifact_output;
pub mod cache;
pub mod hh;
pub use artifact_output::*;
mod resolver;
pub use hh::{HardhatArtifact, HardhatArtifacts};
pub use resolver::Graph;
mod compile;
pub use compile::*;
mod config;
pub use config::{
AllowedLibPaths, Artifact, ArtifactOutput, MinimalCombinedArtifacts, PathStyle,
ProjectPathsConfig, SolcConfig,
pub use compile::{
output::{AggregatedCompilerOutput, ProjectCompileOutput},
*,
};
pub mod remappings;
mod config;
pub use config::{AllowedLibPaths, PathStyle, ProjectPathsConfig, SolcConfig};
use crate::{artifacts::Source, cache::SolFilesCache};
pub mod remappings;
use crate::artifacts::Source;
pub mod error;
pub mod utils;
use crate::{
artifacts::Sources,
cache::PathMap,
artifacts::{Contract, Sources},
error::{SolcError, SolcIoError},
};
use error::Result;
use std::{
borrow::Cow,
collections::BTreeMap,
convert::TryInto,
fmt, fs,
marker::PhantomData,
path::{Path, PathBuf},
};
@ -72,6 +64,8 @@ pub struct Project<Artifacts: ArtifactOutput = MinimalCombinedArtifacts> {
pub allowed_lib_paths: AllowedLibPaths,
/// Maximum number of `solc` processes to run simultaneously.
solc_jobs: usize,
/// Offline mode, if set, network access (download solc) is disallowed
pub offline: bool,
}
impl Project {
@ -120,43 +114,29 @@ impl<Artifacts: ArtifactOutput> Project<Artifacts> {
&self.paths.cache
}
/// Returns the root directory of the project
pub fn root(&self) -> &PathBuf {
&self.paths.root
}
/// Applies the configured settings to the given `Solc`
fn configure_solc(&self, mut solc: Solc) -> Solc {
if self.allowed_lib_paths.0.is_empty() {
solc = solc.arg("--allow-paths").arg(self.allowed_lib_paths.to_string());
}
solc
}
/// Sets the maximum number of parallel `solc` processes to run simultaneously.
///
/// # Panics
///
/// if `jobs == 0`
pub fn set_solc_jobs(&mut self, jobs: usize) {
assert!(jobs > 0);
self.solc_jobs = jobs;
}
#[tracing::instrument(skip_all, name = "Project::write_cache_file")]
fn write_cache_file(
&self,
sources: Sources,
artifacts: Vec<(PathBuf, Vec<String>)>,
) -> Result<()> {
tracing::trace!("inserting {} sources in file cache", sources.len());
let mut cache = SolFilesCache::builder()
.root(&self.paths.root)
.solc_config(self.solc_config.clone())
.insert_files(sources, Some(self.paths.cache.clone()))?;
tracing::trace!("source files inserted");
// 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() {
tracing::trace!("creating cache file parent directory \"{}\"", cache_dir.display());
fs::create_dir_all(cache_dir).map_err(|err| SolcError::io(err, cache_dir))?
}
tracing::trace!("writing cache file to \"{}\"", self.paths.cache.display());
cache.write(&self.paths.cache)?;
Ok(())
}
/// Returns all sources found under the project's configured sources path
#[tracing::instrument(skip_all, fields(name = "sources"))]
pub fn sources(&self) -> Result<Sources> {
@ -187,36 +167,14 @@ impl<Artifacts: ArtifactOutput> Project<Artifacts> {
println!("cargo:rerun-if-changed={}", self.paths.sources.display())
}
/// Attempts to read all unique libraries that are used as imports like "hardhat/console.sol"
fn resolved_libraries(
&self,
sources: &Sources,
) -> Result<BTreeMap<PathBuf, (Source, PathBuf)>> {
let mut libs = BTreeMap::default();
for source in sources.values() {
for import in source.parse_imports() {
if let Some(lib) = utils::resolve_library(&self.paths.libraries, import) {
if let Entry::Vacant(entry) = libs.entry(import.into()) {
tracing::trace!(
"resolved library import \"{}\" at \"{}\"",
import,
lib.display()
);
entry.insert((Source::read(&lib)?, lib));
}
}
}
}
Ok(libs)
}
/// Attempts to compile the contracts found at the configured source location, see
/// `ProjectPathsConfig::sources`.
///
/// NOTE: this does not check if the contracts were successfully compiled, see
/// `CompilerOutput::has_error` instead.
///
/// NB: If the `svm` feature is enabled, this function will automatically detect
/// solc versions across files.
/// solc versions across files, see [`Self::svm_compile()`]
///
/// # Example
///
@ -238,122 +196,37 @@ impl<Artifacts: ArtifactOutput> Project<Artifacts> {
return self.svm_compile(sources)
}
let mut solc = self.solc.clone();
if !self.allowed_lib_paths.0.is_empty() {
solc = solc.arg("--allow-paths").arg(self.allowed_lib_paths.to_string());
}
let solc = self.configure_solc(self.solc.clone());
let sources = Graph::resolve_sources(&self.paths, sources)?.into_sources();
self.compile_with_version(&solc, sources)
}
#[cfg(all(feature = "svm", feature = "async"))]
#[tracing::instrument(skip(self, sources))]
pub fn svm_compile(&self, sources: Sources) -> Result<ProjectCompileOutput<Artifacts>> {
let graph = Graph::resolve_sources(&self.paths, sources)?;
let sources_by_version =
graph.into_sources_by_version(!self.auto_detect)?.get(&self.allowed_lib_paths)?;
// run the compilation step for each version
let compiled = if self.solc_jobs > 1 && sources_by_version.len() > 1 {
self.compile_many(sources_by_version)?
} else {
self.compile_sources(sources_by_version)?
};
tracing::trace!("compiled all sources");
Ok(compiled)
}
/// Compiles all sources with their intended `Solc` version sequentially.
#[cfg(all(feature = "svm", feature = "async"))]
fn compile_sources(
&self,
sources_by_version: BTreeMap<Solc, BTreeMap<PathBuf, Source>>,
) -> Result<ProjectCompileOutput<Artifacts>> {
tracing::trace!("compiling sources using a single solc job");
let mut compiled =
ProjectCompileOutput::with_ignored_errors(self.ignored_error_codes.clone());
for (solc, sources) in sources_by_version {
tracing::trace!(
"compiling {} sources with solc \"{}\"",
sources.len(),
solc.as_ref().display()
);
compiled.extend(self.compile_with_version(&solc, sources)?);
}
Ok(compiled)
}
/// Compiles all sources with their intended `Solc` version in parallel.
/// Compiles a set of contracts using `svm` managed solc installs
///
/// This runs `Self::solc_jobs` parallel `solc` jobs at most.
/// This will autodetect the appropriate `Solc` version(s) to use when compiling the provided
/// `Sources`. Solc auto-detection follows semver rules, see also
/// [`crate::resolver::Graph::get_input_node_versions()`]
///
/// # Errors
///
/// This returns an error if contracts in the `Sources` set are incompatible (violate semver
/// rules) with their imports, for example source contract `A(=0.8.11)` imports dependency
/// `C(<0.8.0)`, which are incompatible.
///
/// # Example
///
/// ```
/// use ethers_solc::{artifacts::Source, Project, utils};
/// # fn demo(project: Project) {
/// let project = Project::builder().build().unwrap();
/// let files = utils::source_files("./src");
/// let sources = Source::read_all(files).unwrap();
/// let output = project.svm_compile(sources).unwrap();
/// # }
/// ```
#[cfg(all(feature = "svm", feature = "async"))]
fn compile_many(
&self,
sources_by_version: BTreeMap<Solc, BTreeMap<PathBuf, Source>>,
) -> Result<ProjectCompileOutput<Artifacts>> {
tracing::trace!("compile sources in parallel using {} solc jobs", self.solc_jobs);
let mut compiled =
ProjectCompileOutput::with_ignored_errors(self.ignored_error_codes.clone());
let mut paths = PathMap::default();
let mut jobs = Vec::with_capacity(sources_by_version.len());
let mut all_sources = BTreeMap::default();
let mut all_artifacts = Vec::with_capacity(sources_by_version.len());
// preprocess all sources
for (solc, sources) in sources_by_version {
match self.preprocess_sources(sources)? {
PreprocessedJob::Unchanged(artifacts) => {
compiled.extend(ProjectCompileOutput::from_unchanged(artifacts));
}
PreprocessedJob::Items(sources, map, cached_artifacts) => {
tracing::trace!("cached artifacts: \"{:?}\"", cached_artifacts.keys());
tracing::trace!("compile sources: \"{:?}\"", sources.keys());
compiled.extend_artifacts(cached_artifacts);
// replace absolute path with source name to make solc happy
let sources = map.set_source_names(sources);
paths.extend(map);
let input = CompilerInput::with_sources(sources)
.settings(self.solc_config.settings.clone())
.normalize_evm_version(&solc.version()?)
.with_remappings(self.paths.remappings.clone());
jobs.push((solc, input))
}
};
}
tracing::trace!("execute {} compile jobs in parallel", jobs.len());
let outputs = tokio::runtime::Runtime::new()
.unwrap()
.block_on(Solc::compile_many(jobs, self.solc_jobs));
for (res, _, input) in outputs.into_outputs() {
let output = res?;
if !output.has_error() {
if self.cached {
// get all contract names of the files and map them to the disk file
all_sources.extend(paths.set_disk_paths(input.sources));
all_artifacts.extend(paths.get_artifacts(&output.contracts));
}
if !self.no_artifacts {
Artifacts::on_output(&output, &self.paths)?;
}
}
compiled.extend_output(output);
}
// write the cache file
if self.cached {
self.write_cache_file(all_sources, all_artifacts)?;
}
Ok(compiled)
pub fn svm_compile(&self, sources: Sources) -> Result<ProjectCompileOutput<Artifacts>> {
project::ProjectCompiler::with_sources(self, sources)?.compile()
}
/// Compiles the given source files with the exact `Solc` executable
@ -384,121 +257,33 @@ impl<Artifacts: ArtifactOutput> Project<Artifacts> {
solc: &Solc,
sources: Sources,
) -> Result<ProjectCompileOutput<Artifacts>> {
let (sources, paths, cached_artifacts) = match self.preprocess_sources(sources)? {
PreprocessedJob::Unchanged(artifacts) => {
return Ok(ProjectCompileOutput::from_unchanged(artifacts))
}
PreprocessedJob::Items(a, b, c) => (a, b, c),
};
let version = solc.version()?;
tracing::trace!(
"compiling {} files with {}. Using {} cached files",
sources.len(),
version,
cached_artifacts.len()
);
tracing::trace!("cached artifacts: \"{:?}\"", cached_artifacts.keys());
tracing::trace!("compile sources: \"{:?}\"", sources.keys());
// replace absolute path with source name to make solc happy
let sources = paths.set_source_names(sources);
let input = CompilerInput::with_sources(sources)
.settings(self.solc_config.settings.clone())
.normalize_evm_version(&version)
.with_remappings(self.paths.remappings.clone());
tracing::trace!("calling solc with {} sources", input.sources.len());
let output = solc.compile(&input)?;
tracing::trace!("compiled input, output has error: {}", output.has_error());
if output.has_error() {
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 = paths.get_artifacts(&output.contracts);
// reapply to disk paths
let sources = paths.set_disk_paths(input.sources);
// create cache file
self.write_cache_file(sources, artifacts)?;
}
// 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_and_cache(
output,
cached_artifacts,
self.ignored_error_codes.clone(),
))
}
/// Preprocesses the given source files by resolving their libs and check against cache if
/// configured
fn preprocess_sources(&self, mut sources: Sources) -> Result<PreprocessedJob<Artifacts>> {
tracing::trace!("start preprocessing {} sources files", sources.len());
// keeps track of source names / disk paths
let mut paths = PathMap::default();
tracing::trace!("start resolving libraries");
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);
paths.path_to_source_name.insert(path.clone(), import.clone());
paths.source_name_to_path.insert(import, path);
}
tracing::trace!("resolved all libraries");
// If there's a cache set, filter to only re-compile the files which were changed
let (sources, cached_artifacts) = if self.cached && self.paths.cache.exists() {
tracing::trace!("start reading solfiles cache for incremental compilation");
let mut cache = SolFilesCache::read(&self.paths.cache)?;
cache.remove_missing_files();
let changed_files = cache.get_changed_or_missing_artifacts_files::<Artifacts>(
sources,
Some(&self.solc_config),
&self.paths,
);
tracing::trace!("detected {} changed files", changed_files.len());
cache.remove_changed_files(&changed_files);
let cached_artifacts = if self.paths.artifacts.exists() {
tracing::trace!("reading artifacts from cache..");
let artifacts = cache.read_artifacts::<Artifacts>(&self.paths.artifacts)?;
tracing::trace!("read {} artifacts from cache", artifacts.len());
artifacts
} else {
BTreeMap::default()
};
// if nothing changed and all artifacts still exist
if changed_files.is_empty() {
tracing::trace!(
"unchanged source files, reusing artifacts {:?}",
cached_artifacts.keys()
);
return Ok(PreprocessedJob::Unchanged(cached_artifacts))
}
// There are changed files and maybe some cached files
(changed_files, cached_artifacts)
} else {
(sources, BTreeMap::default())
};
Ok(PreprocessedJob::Items(sources, paths, cached_artifacts))
project::ProjectCompiler::with_sources_and_solc(
self,
sources,
self.configure_solc(solc.clone()),
)?
.compile()
}
/// Removes the project's artifacts and cache file
///
/// If the cache file was the only file in the folder, this also removes the empty folder.
///
/// # Example
///
/// ```
/// use ethers_solc::Project;
/// # fn demo(project: Project) {
/// let project = Project::builder().build().unwrap();
/// let _ = project.compile().unwrap();
/// assert!(project.artifacts_path().exists());
/// assert!(project.cache_path().exists());
///
/// project.cleanup();
/// assert!(!project.artifacts_path().exists());
/// assert!(!project.cache_path().exists());
/// # }
/// ```
pub fn cleanup(&self) -> std::result::Result<(), SolcIoError> {
tracing::trace!("clean up project");
if self.cache_path().exists() {
@ -526,24 +311,19 @@ impl<Artifacts: ArtifactOutput> Project<Artifacts> {
Ok(())
}
/// Flattens the target file into a single string suitable for verification
/// Flattens the target solidity file into a single string suitable for verification.
///
/// This method uses a dependency graph to resolve imported files and substitute
/// import directives with the contents of target files. It will strip the pragma
/// version directives and SDPX license identifiers from imported files.
/// version directives and SDPX license identifiers from all imported files.
///
/// NOTE: the SDPX license identifier will be removed from the imported file
/// NB: the SDPX license identifier will be removed from the imported file
/// only if it is found at the beginning of the file.
pub fn flatten(&self, target: &Path) -> Result<String> {
self.paths.flatten(target)
}
}
enum PreprocessedJob<T: ArtifactOutput> {
Unchanged(BTreeMap<PathBuf, T::Artifact>),
Items(Sources, PathMap, BTreeMap<PathBuf, T::Artifact>),
}
pub struct ProjectBuilder<Artifacts: ArtifactOutput = MinimalCombinedArtifacts> {
/// The layout of the
paths: Option<ProjectPathsConfig>,
@ -557,6 +337,8 @@ pub struct ProjectBuilder<Artifacts: ArtifactOutput = MinimalCombinedArtifacts>
no_artifacts: bool,
/// Whether automatic solc version detection is enabled
auto_detect: bool,
/// Use offline mode
offline: bool,
artifacts: PhantomData<Artifacts>,
/// Which error codes to ignore
pub ignored_error_codes: Vec<u64>,
@ -611,6 +393,21 @@ impl<Artifacts: ArtifactOutput> ProjectBuilder<Artifacts> {
self
}
/// Activates offline mode
///
/// Prevents network possible access to download/check solc installs
#[must_use]
pub fn offline(self) -> Self {
self.set_offline(true)
}
/// Sets the offline status
#[must_use]
pub fn set_offline(mut self, offline: bool) -> Self {
self.offline = offline;
self
}
/// Disables writing artifacts to disk
#[must_use]
pub fn no_artifacts(self) -> Self {
@ -667,6 +464,7 @@ impl<Artifacts: ArtifactOutput> ProjectBuilder<Artifacts> {
ignored_error_codes,
allowed_paths,
solc_jobs,
offline,
..
} = self;
ProjectBuilder {
@ -676,6 +474,7 @@ impl<Artifacts: ArtifactOutput> ProjectBuilder<Artifacts> {
cached,
no_artifacts,
auto_detect,
offline,
artifacts: PhantomData::default(),
ignored_error_codes,
allowed_paths,
@ -715,13 +514,14 @@ impl<Artifacts: ArtifactOutput> ProjectBuilder<Artifacts> {
ignored_error_codes,
mut allowed_paths,
solc_jobs,
offline,
} = self;
let paths = paths.map(Ok).unwrap_or_else(ProjectPathsConfig::current_hardhat)?;
let solc = solc.unwrap_or_default();
let solc_config = solc_config.unwrap_or_else(|| SolcConfig::builder().build());
let paths = paths.map(Ok).unwrap_or_else(ProjectPathsConfig::current_hardhat)?;
if allowed_paths.is_empty() {
// allow every contract under root by default
allowed_paths.push(paths.root.clone())
@ -736,8 +536,9 @@ impl<Artifacts: ArtifactOutput> ProjectBuilder<Artifacts> {
auto_detect,
artifacts,
ignored_error_codes,
allowed_lib_paths: allowed_paths.try_into()?,
allowed_lib_paths: allowed_paths.into(),
solc_jobs: solc_jobs.unwrap_or_else(::num_cpus::get),
offline,
})
}
}
@ -751,6 +552,7 @@ impl<Artifacts: ArtifactOutput> Default for ProjectBuilder<Artifacts> {
cached: true,
no_artifacts: false,
auto_detect: true,
offline: false,
artifacts: PhantomData::default(),
ignored_error_codes: Vec::new(),
allowed_paths: vec![],
@ -759,205 +561,18 @@ impl<Artifacts: ArtifactOutput> Default for ProjectBuilder<Artifacts> {
}
}
/// The outcome of `Project::compile`
#[derive(Debug, Clone, PartialEq, Default)]
pub struct ProjectCompileOutput<T: ArtifactOutput> {
/// 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<CompilerOutput>,
/// All artifacts that were read from cache
artifacts: BTreeMap<PathBuf, T::Artifact>,
ignored_error_codes: Vec<u64>,
}
impl<Artifacts: ArtifactOutput> ArtifactOutput for Project<Artifacts> {
type Artifact = Artifacts::Artifact;
impl<T: ArtifactOutput> ProjectCompileOutput<T> {
pub fn with_ignored_errors(ignored_errors: Vec<u64>) -> Self {
Self {
compiler_output: None,
artifacts: Default::default(),
ignored_error_codes: ignored_errors,
}
}
pub fn from_unchanged(artifacts: BTreeMap<PathBuf, T::Artifact>) -> Self {
Self { compiler_output: None, artifacts, ignored_error_codes: vec![] }
}
pub fn from_compiler_output(
compiler_output: CompilerOutput,
ignored_error_codes: Vec<u64>,
) -> Self {
Self {
compiler_output: Some(compiler_output),
artifacts: Default::default(),
ignored_error_codes,
}
}
pub fn from_compiler_output_and_cache(
compiler_output: CompilerOutput,
cache: BTreeMap<PathBuf, T::Artifact>,
ignored_error_codes: Vec<u64>,
) -> Self {
Self { compiler_output: Some(compiler_output), artifacts: cache, 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<String, Contract> =
/// 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<T>) {
let ProjectCompileOutput { compiler_output, artifacts, .. } = compiled;
self.artifacts.extend(artifacts);
if let Some(output) = compiler_output {
self.extend_output(output);
}
}
pub fn extend_output(&mut self, compiled: CompilerOutput) {
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);
}
}
pub fn extend_artifacts(&mut self, artifacts: BTreeMap<PathBuf, T::Artifact>) {
self.artifacts.extend(artifacts);
}
/// 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 {
self.compiler_output.as_ref().map(|o| o.has_error()).unwrap_or_default()
}
/// Whether there were warnings
pub fn has_compiler_warnings(&self) -> bool {
self.compiler_output
.as_ref()
.map(|o| o.has_warning(&self.ignored_error_codes))
.unwrap_or_default()
}
/// Finds the first contract with the given name and removes it from the set
pub fn remove(&mut self, contract_name: impl AsRef<str>) -> Option<T::Artifact> {
let contract_name = contract_name.as_ref();
if let Some(output) = self.compiler_output.as_mut() {
if let contract @ Some(_) = output.contracts.iter_mut().find_map(|(file, c)| {
c.remove(contract_name).map(|c| T::contract_to_artifact(file, contract_name, c))
}) {
return contract
}
}
let key = self
.artifacts
.iter()
.find_map(|(path, _)| {
T::contract_name(path).filter(|name| name == contract_name).map(|_| path)
})?
.clone();
self.artifacts.remove(&key)
}
}
impl<T: ArtifactOutput> ProjectCompileOutput<T>
where
T::Artifact: Clone,
{
/// Finds the first contract with the given name
pub fn find(&self, contract_name: impl AsRef<str>) -> Option<Cow<T::Artifact>> {
let contract_name = contract_name.as_ref();
if let Some(output) = self.compiler_output.as_ref() {
if let contract @ Some(_) = output.contracts.iter().find_map(|(file, contracts)| {
contracts
.get(contract_name)
.map(|c| T::contract_to_artifact(file, contract_name, c.clone()))
.map(Cow::Owned)
}) {
return contract
}
}
self.artifacts.iter().find_map(|(path, art)| {
T::contract_name(path).filter(|name| name == contract_name).map(|_| Cow::Borrowed(art))
})
}
}
impl<T: ArtifactOutput + 'static> ProjectCompileOutput<T> {
/// All artifacts together with their contract file name and name `<file name>:<name>`
///
/// # Example
///
/// ```no_run
/// use std::collections::BTreeMap;
/// use ethers_solc::artifacts::CompactContractBytecode;
/// use ethers_solc::Project;
///
/// let project = Project::builder().build().unwrap();
/// let contracts: BTreeMap<String, CompactContractBytecode> = project.compile().unwrap().into_artifacts().collect();
/// ```
pub fn into_artifacts(mut self) -> Box<dyn Iterator<Item = (String, T::Artifact)>> {
let artifacts = self.artifacts.into_iter().filter_map(|(path, art)| {
T::contract_name(&path).map(|name| {
(format!("{}:{}", path.file_name().unwrap().to_string_lossy(), name), art)
})
});
let artifacts: Box<dyn Iterator<Item = (String, T::Artifact)>> = if let Some(output) =
self.compiler_output.take()
{
Box::new(artifacts.chain(T::output_to_artifacts(output).into_values().flatten().map(
|(name, artifact)| {
(format!("{}:{}", T::output_file_name(&name).display(), name), artifact)
},
)))
} else {
Box::new(artifacts)
};
artifacts
}
}
impl<T: ArtifactOutput> fmt::Display for ProjectCompileOutput<T> {
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")
}
fn contract_to_artifact(file: &str, name: &str, contract: Contract) -> Self::Artifact {
Artifacts::contract_to_artifact(file, name, contract)
}
}
#[cfg(test)]
mod tests {
use crate::remappings::Remapping;
#[test]
#[cfg(all(feature = "svm", feature = "async"))]
fn test_build_all_versions() {
@ -973,7 +588,7 @@ mod tests {
assert!(!compiled.has_compiler_errors());
let contracts = compiled.output().contracts;
// Contracts A to F
assert_eq!(contracts.keys().count(), 5);
assert_eq!(contracts.contracts().count(), 5);
}
#[test]
@ -988,6 +603,11 @@ mod tests {
.sources(root.join("src"))
.lib(root.join("lib1"))
.lib(root.join("lib2"))
.remappings(
Remapping::find_many(&root.join("lib1"))
.into_iter()
.chain(Remapping::find_many(&root.join("lib2"))),
)
.build()
.unwrap();
let project = Project::builder()
@ -1000,7 +620,7 @@ mod tests {
let compiled = project.compile().unwrap();
assert!(!compiled.has_compiler_errors());
let contracts = compiled.output().contracts;
assert_eq!(contracts.keys().count(), 3);
assert_eq!(contracts.contracts().count(), 3);
}
#[test]
@ -1013,12 +633,13 @@ mod tests {
.root(&root)
.sources(root.join("src"))
.lib(root.join("lib"))
.remappings(Remapping::find_many(&root.join("lib")))
.build()
.unwrap();
let project = Project::builder().no_artifacts().paths(paths).ephemeral().build().unwrap();
let compiled = project.compile().unwrap();
assert!(!compiled.has_compiler_errors());
let contracts = compiled.output().contracts;
assert_eq!(contracts.keys().count(), 2);
assert_eq!(contracts.contracts().count(), 2);
}
}

View File

@ -77,6 +77,21 @@ impl<T: ArtifactOutput> TempProject<T> {
&mut self.project_mut().paths
}
/// Returns the path to the artifacts directory
pub fn artifacts_path(&self) -> &PathBuf {
&self.paths().artifacts
}
/// Returns the path to the sources directory
pub fn sources_path(&self) -> &PathBuf {
&self.paths().sources
}
/// Returns the path to the cache file
pub fn cache_path(&self) -> &PathBuf {
&self.paths().cache
}
/// The root path of the temporary workspace
pub fn root(&self) -> &Path {
self.project().paths.root.as_path()

View File

@ -38,20 +38,71 @@ use solang_parser::pt::{Import, Loc, SourceUnitPart};
use crate::{error::Result, utils, ProjectPathsConfig, Solc, Source, Sources};
/// The underlying edges of the graph which only contains the raw relationship data.
///
/// This is kept separate from the `Graph` as the `Node`s get consumed when the `Solc` to `Sources`
/// set is determined.
#[derive(Debug, Clone)]
pub struct GraphEdges {
/// The indices of `edges` correspond to the `nodes`. That is, `edges[0]`
/// is the set of outgoing edges for `nodes[0]`.
edges: Vec<Vec<usize>>,
/// index maps for a solidity file to an index, for fast lookup.
indices: HashMap<PathBuf, usize>,
/// reverse of `indices` for reverse lookup
rev_indices: HashMap<usize, PathBuf>,
/// the identified version requirement of a file
versions: HashMap<usize, Option<VersionReq>>,
/// with how many input files we started with, corresponds to `let input_files =
/// nodes[..num_input_files]`.
///
/// Combined with the `indices` this way we can determine if a file was original added to the
/// graph as input or was added as resolved import, see [`Self::is_input_file()`]
num_input_files: usize,
}
impl GraphEdges {
/// Returns a list of nodes the given node index points to for the given kind.
pub fn imported_nodes(&self, from: usize) -> &[usize] {
&self.edges[from]
}
/// Returns all files imported by the given file
pub fn imports(&self, file: impl AsRef<Path>) -> HashSet<&PathBuf> {
if let Some(start) = self.indices.get(file.as_ref()).copied() {
NodesIter::new(start, self).skip(1).map(move |idx| &self.rev_indices[&idx]).collect()
} else {
HashSet::new()
}
}
/// Returns true if the `file` was originally included when the graph was first created and not
/// added when all `imports` were resolved
pub fn is_input_file(&self, file: impl AsRef<Path>) -> bool {
if let Some(idx) = self.indices.get(file.as_ref()).copied() {
idx < self.num_input_files
} else {
false
}
}
/// Returns the `VersionReq` for the given file
pub fn version_requirement(&self, file: impl AsRef<Path>) -> Option<&VersionReq> {
self.indices
.get(file.as_ref())
.and_then(|idx| self.versions.get(idx))
.and_then(|v| v.as_ref())
}
}
/// Represents a fully-resolved solidity dependency graph. Each node in the graph
/// is a file and edges represent dependencies between them.
/// See also https://docs.soliditylang.org/en/latest/layout-of-source-files.html?highlight=import#importing-other-source-files
#[derive(Debug)]
pub struct Graph {
nodes: Vec<Node>,
/// The indices of `edges` correspond to the `nodes`. That is, `edges[0]`
/// is the set of outgoing edges for `nodes[0]`.
edges: Vec<Vec<usize>>,
/// index maps for a solidity file to an index, for fast lookup.
indices: HashMap<PathBuf, usize>,
/// with how many input files we started with, corresponds to `let input_files =
/// nodes[..num_input_files]`.
num_input_files: usize,
/// relationship of the nodes
edges: GraphEdges,
/// the root of the project this graph represents
#[allow(unused)]
root: PathBuf,
@ -60,15 +111,19 @@ pub struct Graph {
impl Graph {
/// Returns a list of nodes the given node index points to for the given kind.
pub fn imported_nodes(&self, from: usize) -> &[usize] {
&self.edges[from]
self.edges.imported_nodes(from)
}
/// Returns all the resolved files and their index in the graph
pub fn files(&self) -> &HashMap<PathBuf, usize> {
&self.indices
&self.edges.indices
}
/// Gets a node by index.
///
/// # Panics
///
/// if the `index` node id is not included in the graph
pub fn node(&self, index: usize) -> &Node {
&self.nodes[index]
}
@ -80,7 +135,7 @@ impl Graph {
///
/// if the `start` node id is not included in the graph
pub fn node_ids(&self, start: usize) -> impl Iterator<Item = usize> + '_ {
NodesIter::new(start, self)
NodesIter::new(start, &self.edges)
}
/// Same as `Self::node_ids` but returns the actual `Node`
@ -88,16 +143,22 @@ impl Graph {
self.node_ids(start).map(move |idx| self.node(idx))
}
/// Returns all files together with their paths
pub fn into_sources(self) -> Sources {
self.nodes.into_iter().map(|node| (node.path, node.source)).collect()
/// Consumes the `Graph`, effectively splitting the `nodes` and the `GraphEdges` off and
/// returning the `nodes` converted to `Sources`
pub fn into_sources(self) -> (Sources, GraphEdges) {
let Graph { nodes, edges, .. } = self;
(nodes.into_iter().map(|node| (node.path, node.source)).collect(), edges)
}
/// Returns an iterator that yields only those nodes that represent input files.
/// See `Self::resolve_sources`
/// This won't yield any resolved library nodes
pub fn input_nodes(&self) -> impl Iterator<Item = &Node> {
self.nodes.iter().take(self.num_input_files)
self.nodes.iter().take(self.edges.num_input_files)
}
pub fn imports(&self, path: impl AsRef<Path>) -> HashSet<&PathBuf> {
self.edges.imports(path)
}
/// Resolves a number of sources within the given config
@ -164,8 +225,18 @@ impl Graph {
nodes.push(node);
edges.push(resolved_imports);
}
Ok(Graph { nodes, edges, indices: index, num_input_files, root: paths.root.clone() })
let edges = GraphEdges {
edges,
rev_indices: index.iter().map(|(k, v)| (*v, k.clone())).collect(),
indices: index,
num_input_files,
versions: nodes
.iter()
.enumerate()
.map(|(idx, node)| (idx, node.data.version_req.clone()))
.collect(),
};
Ok(Graph { nodes, edges, root: paths.root.clone() })
}
/// Resolves the dependencies of a project's source contracts
@ -176,11 +247,12 @@ impl Graph {
#[cfg(all(feature = "svm", feature = "async"))]
impl Graph {
/// Returns all input files together with their appropriate version.
/// Consumes the nodes of the graph and returns all input files together with their appropriate
/// version and the edges of the graph
///
/// First we determine the compatible version for each input file (from sources and test folder,
/// see `Self::resolve`) and then we add all resolved library imports.
pub fn into_sources_by_version(self, offline: bool) -> Result<VersionedSources> {
pub fn into_sources_by_version(self, offline: bool) -> Result<(VersionedSources, GraphEdges)> {
/// insert the imports of the given node into the sources map
/// There can be following graph:
/// `A(<=0.8.10) imports C(>0.4.0)` and `B(0.8.11) imports C(>0.4.0)`
@ -209,7 +281,7 @@ impl Graph {
}
let versioned_nodes = self.get_input_node_versions(offline)?;
let Self { nodes, edges, num_input_files, .. } = self;
let Self { nodes, edges, .. } = self;
let mut versioned_sources = HashMap::with_capacity(versioned_nodes.len());
let mut all_nodes = nodes.into_iter().enumerate().collect::<HashMap<_, _>>();
@ -221,11 +293,17 @@ impl Graph {
// insert the input node in the sources set and remove it from the available set
let node = all_nodes.remove(&idx).expect("node is preset. qed");
sources.insert(node.path, node.source);
insert_imports(idx, &mut all_nodes, &mut sources, &edges, num_input_files);
insert_imports(
idx,
&mut all_nodes,
&mut sources,
&edges.edges,
edges.num_input_files,
);
}
versioned_sources.insert(version, sources);
}
Ok(VersionedSources { inner: versioned_sources, offline })
Ok((VersionedSources { inner: versioned_sources, offline }, edges))
}
/// Writes the list of imported files into the given formatter:
@ -294,7 +372,8 @@ impl Graph {
// on first error, instead gather all the errors and return a bundled error message instead
let mut errors = Vec::new();
// we also don't want duplicate error diagnostic
let mut erroneous_nodes = std::collections::HashSet::with_capacity(self.num_input_files);
let mut erroneous_nodes =
std::collections::HashSet::with_capacity(self.edges.num_input_files);
let all_versions = if offline { Solc::installed_versions() } else { Solc::all_versions() };
@ -302,7 +381,7 @@ impl Graph {
let mut versioned_nodes = HashMap::new();
// walking through the node's dep tree and filtering the versions along the way
for idx in 0..self.num_input_files {
for idx in 0..self.edges.num_input_files {
let mut candidates = all_versions.iter().collect::<Vec<_>>();
self.retain_compatible_versions(idx, &mut candidates);
@ -346,12 +425,12 @@ pub struct NodesIter<'a> {
/// stack of nodes
stack: VecDeque<usize>,
visited: HashSet<usize>,
graph: &'a Graph,
graph: &'a GraphEdges,
}
impl<'a> NodesIter<'a> {
fn new(start: usize, graph: &'a Graph) -> Self {
Self { stack: VecDeque::from([start]), visited: Default::default(), graph }
fn new(start: usize, graph: &'a GraphEdges) -> Self {
Self { stack: VecDeque::from([start]), visited: HashSet::new(), graph }
}
}
@ -382,7 +461,7 @@ impl VersionedSources {
pub fn get(
self,
allowed_lib_paths: &crate::AllowedLibPaths,
) -> Result<std::collections::BTreeMap<Solc, Sources>> {
) -> Result<std::collections::BTreeMap<Solc, (semver::Version, Sources)>> {
use crate::SolcError;
// we take the installer lock here to ensure installation checking is done in sync
@ -411,8 +490,9 @@ impl VersionedSources {
Solc::blocking_install(version.as_ref())?;
tracing::trace!("reinstalled solc: \"{}\"", version);
}
sources_by_version
.insert(solc.arg("--allow-paths").arg(allowed_lib_paths.to_string()), sources);
let solc = solc.arg("--allow-paths").arg(allowed_lib_paths.to_string());
let version = solc.version()?;
sources_by_version.insert(solc, (version, sources));
}
Ok(sources_by_version)
}
@ -596,7 +676,7 @@ mod tests {
let graph = Graph::resolve(&paths).unwrap();
assert_eq!(graph.num_input_files, 1);
assert_eq!(graph.edges.num_input_files, 1);
assert_eq!(graph.files().len(), 2);
assert_eq!(
@ -615,7 +695,7 @@ mod tests {
let graph = Graph::resolve(&paths).unwrap();
assert_eq!(graph.num_input_files, 2);
assert_eq!(graph.edges.num_input_files, 2);
assert_eq!(graph.files().len(), 3);
assert_eq!(
graph.files().clone(),

View File

@ -6,6 +6,7 @@ use crate::{error::SolcError, SolcIoError};
use once_cell::sync::Lazy;
use regex::{Match, Regex};
use semver::Version;
use serde::de::DeserializeOwned;
use tiny_keccak::{Hasher, Keccak};
use walkdir::WalkDir;
@ -82,6 +83,20 @@ pub fn canonicalize(path: impl AsRef<Path>) -> Result<PathBuf, SolcIoError> {
dunce::canonicalize(&path).map_err(|err| SolcIoError::new(err, path))
}
/// Returns the same path config but with canonicalized paths.
///
/// This will take care of potential symbolic linked directories.
/// For example, the tempdir library is creating directories hosted under `/var/`, which in OS X
/// is a symbolic link to `/private/var/`. So if when we try to resolve imports and a path is
/// rooted in a symbolic directory we might end up with different paths for the same file, like
/// `private/var/.../Dapp.sol` and `/var/.../Dapp.sol`
///
/// This canonicalizes all the paths but does not treat non existing dirs as an error
pub fn canonicalized(path: impl Into<PathBuf>) -> PathBuf {
let path = path.into();
canonicalize(&path).unwrap_or(path)
}
/// Returns the path to the library if the source path is in fact determined to be a library path,
/// and it exists.
/// Note: this does not handle relative imports or remappings.
@ -252,6 +267,31 @@ pub(crate) fn tempdir(name: &str) -> Result<tempfile::TempDir, SolcIoError> {
tempfile::Builder::new().prefix(name).tempdir().map_err(|err| SolcIoError::new(err, name))
}
/// Reads the json file and deserialize it into the provided type
pub fn read_json_file<T: DeserializeOwned>(path: impl AsRef<Path>) -> Result<T, SolcError> {
let path = path.as_ref();
let file = std::fs::File::open(path).map_err(|err| SolcError::io(err, path))?;
let file = std::io::BufReader::new(file);
let val: T = serde_json::from_reader(file)?;
Ok(val)
}
/// Creates the parent directory of the `file` and all its ancestors if it does not exist
/// See [`std::fs::create_dir_all()`]
pub fn create_parent_dir_all(file: impl AsRef<Path>) -> Result<(), SolcError> {
let file = file.as_ref();
if let Some(parent) = file.parent() {
std::fs::create_dir_all(parent).map_err(|err| {
SolcError::msg(format!(
"Failed to create artifact parent folder \"{}\": {}",
parent.display(),
err
))
})?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;

View File

@ -1,12 +1,11 @@
{
"_format": "hh-sol-cache-2",
"_format": "ethers-rs-sol-cache-2",
"files": {
"Greeter.sol": {
"lastModificationDate": 1634246369587,
"contentHash": "483b7f4f64b06a04a24bd0af7c3bf8b7",
"sourceName": "contracts/Greeter.sol",
"/Users/Matthias/git/rust/ethers-rs/ethers-solc/test-data/dapp-sample/lib/ds-test/src/test.sol": {
"lastModificationDate": 1638218667720,
"contentHash": "5d45a46528eaf8a26f0a8d93669f3148",
"sourceName": "/Users/Matthias/git/rust/ethers-rs/ethers-solc/test-data/dapp-sample/lib/ds-test/src/test.sol",
"solcConfig": {
"version": "0.8.4",
"settings": {
"optimizer": {
"enabled": false,
@ -14,62 +13,98 @@
},
"outputSelection": {
"*": {
"": [
"ast"
],
"*": [
"abi",
"evm.bytecode",
"evm.deployedBytecode",
"evm.methodIdentifiers"
],
"": [
"ast"
]
}
}
}
},
"imports": [
"hardhat/console.sol"
],
"versionPragmas": [
"^0.8.0"
],
"artifacts": [
"Greeter"
]
},
"console.sol": {
"lastModificationDate": 1634245289287,
"contentHash": "cc4777addd464ea56fa35b1c45df0591",
"sourceName": "hardhat/console.sol",
"solcConfig": {
"version": "0.8.4",
"settings": {
"optimizer": {
"enabled": false,
"runs": 200
},
"outputSelection": {
"*": {
"*": [
"abi",
"evm.bytecode",
"evm.deployedBytecode",
"evm.methodIdentifiers"
],
"": [
"ast"
]
}
}
"evmVersion": "london"
}
},
"imports": [],
"versionPragmas": [
">=0.4.22 <0.9.0"
"versionRequirement": ">=0.4.23",
"artifacts": {
"DSTest": {
"0.8.11+commit.d7f03943.Darwin.appleclang": "test.sol/DSTest.json"
}
}
},
"/Users/Matthias/git/rust/ethers-rs/ethers-solc/test-data/dapp-sample/src/Dapp.sol": {
"lastModificationDate": 1638193396942,
"contentHash": "a41ddb3b99ae6b72b59341eabf948542",
"sourceName": "/Users/Matthias/git/rust/ethers-rs/ethers-solc/test-data/dapp-sample/src/Dapp.sol",
"solcConfig": {
"settings": {
"optimizer": {
"enabled": false,
"runs": 200
},
"outputSelection": {
"*": {
"": [
"ast"
],
"*": [
"abi",
"evm.bytecode",
"evm.deployedBytecode",
"evm.methodIdentifiers"
]
}
},
"evmVersion": "london"
}
},
"imports": [],
"versionRequirement": ">=0.6.6",
"artifacts": {
"Dapp": {
"0.8.11+commit.d7f03943.Darwin.appleclang": "Dapp.sol/Dapp.json"
}
}
},
"/Users/Matthias/git/rust/ethers-rs/ethers-solc/test-data/dapp-sample/src/Dapp.t.sol": {
"lastModificationDate": 1638193396942,
"contentHash": "5f5038d89f69269d0734659efaa2ec52",
"sourceName": "/Users/Matthias/git/rust/ethers-rs/ethers-solc/test-data/dapp-sample/src/Dapp.t.sol",
"solcConfig": {
"settings": {
"optimizer": {
"enabled": false,
"runs": 200
},
"outputSelection": {
"*": {
"": [
"ast"
],
"*": [
"abi",
"evm.bytecode",
"evm.deployedBytecode",
"evm.methodIdentifiers"
]
}
},
"evmVersion": "london"
}
},
"imports": [
"/Users/Matthias/git/rust/ethers-rs/ethers-solc/test-data/dapp-sample/lib/ds-test/src/test.sol",
"/Users/Matthias/git/rust/ethers-rs/ethers-solc/test-data/dapp-sample/src/Dapp.sol"
],
"artifacts": [
"console"
]
"versionRequirement": ">=0.6.6",
"artifacts": {
"DappTest": {
"0.8.11+commit.d7f03943.Darwin.appleclang": "Dapp.t.sol/DappTest.json"
}
}
}
}
}
}

View File

@ -1,6 +1,6 @@
pragma solidity 0.8.6;
import "../lib1/Bar.sol";
import "../lib2/Baz.sol";
import "bar/Bar.sol";
import "baz/Baz.sol";
contract Foo is Bar, Baz {}

View File

@ -1,7 +1,7 @@
//! project tests
use std::{
collections::HashMap,
collections::{HashMap, HashSet},
io,
path::{Path, PathBuf},
str::FromStr,
@ -11,8 +11,16 @@ use ethers_solc::{
cache::{SolFilesCache, SOLIDITY_FILES_CACHE_FILENAME},
project_util::*,
remappings::Remapping,
Graph, MinimalCombinedArtifacts, Project, ProjectPathsConfig,
Graph, MinimalCombinedArtifacts, Project, ProjectCompileOutput, ProjectPathsConfig,
};
use pretty_assertions::assert_eq;
#[allow(unused)]
fn init_tracing() {
tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.init();
}
#[test]
fn can_compile_hardhat_sample() {
@ -56,11 +64,16 @@ fn can_compile_dapp_sample() {
assert!(compiled.find("Dapp").is_some());
assert!(compiled.is_unchanged());
let cache = SolFilesCache::read(project.cache_path()).unwrap();
// 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());
let updated_cache = SolFilesCache::read(project.cache_path()).unwrap();
assert_eq!(cache, updated_cache);
}
#[test]
@ -72,7 +85,6 @@ fn can_compile_dapp_detect_changes_in_libs() {
.paths_mut()
.remappings
.push(Remapping::from_str(&format!("remapping={}/", remapping.display())).unwrap());
project.project_mut().auto_detect = false;
let src = project
.add_source(
@ -139,6 +151,7 @@ fn can_compile_dapp_detect_changes_in_libs() {
#[test]
fn can_compile_dapp_detect_changes_in_sources() {
init_tracing();
let project = TempProject::<MinimalCombinedArtifacts>::dapptools().unwrap();
let src = project
@ -214,6 +227,7 @@ fn can_compile_dapp_detect_changes_in_sources() {
assert!(compiled.find("DssSpellTestBase").is_some());
// ensure change is detected
assert!(!compiled.is_unchanged());
// and all recompiled artifacts are different
for (p, artifact) in compiled.into_artifacts() {
let other = artifacts.remove(&p).unwrap();
@ -266,31 +280,31 @@ fn can_compile_dapp_sample_with_cache() {
assert!(compiled.find("NewContract").is_some());
assert!(!compiled.is_unchanged());
assert_eq!(
compiled.into_artifacts().map(|(name, _)| name).collect::<Vec<_>>(),
vec![
"Dapp.json:Dapp",
"DappTest.json:DappTest",
"DSTest.json:DSTest",
"NewContract.json:NewContract"
]
compiled.into_artifacts().map(|(name, _)| name).collect::<HashSet<_>>(),
HashSet::from([
"Dapp.json:Dapp".to_string(),
"DappTest.json:DappTest".to_string(),
"DSTest.json:DSTest".to_string(),
"NewContract.json:NewContract".to_string(),
])
);
// old cached artifact is not taken from the cache
std::fs::copy(cache_testdata_dir.join("Dapp.sol"), root.join("src/Dapp.sol")).unwrap();
let compiled = project.compile().unwrap();
assert_eq!(
compiled.into_artifacts().map(|(name, _)| name).collect::<Vec<_>>(),
vec![
"DappTest.json:DappTest",
"NewContract.json:NewContract",
"DSTest.json:DSTest",
"Dapp.json:Dapp"
]
compiled.into_artifacts().map(|(name, _)| name).collect::<HashSet<_>>(),
HashSet::from([
"DappTest.json:DappTest".to_string(),
"NewContract.json:NewContract".to_string(),
"DSTest.json:DSTest".to_string(),
"Dapp.json:Dapp".to_string(),
])
);
// deleted artifact is not taken from the cache
std::fs::remove_file(&project.paths.sources.join("Dapp.sol")).unwrap();
let compiled = project.compile().unwrap();
let compiled: ProjectCompileOutput<_> = project.compile().unwrap();
assert!(compiled.find("Dapp").is_none());
}
@ -376,7 +390,7 @@ fn can_flatten_file_with_duplicates() {
assert_eq!(result.matches("contract Foo {").count(), 1);
assert_eq!(result.matches("contract Bar {").count(), 1);
assert_eq!(result.matches("contract FooBar {").count(), 1);
assert_eq!(result.matches(";").count(), 1);
assert_eq!(result.matches(';').count(), 1);
}
#[test]