2021-10-30 17:59:44 +00:00
|
|
|
#![doc = include_str!("../README.md")]
|
2021-10-26 11:28:10 +00:00
|
|
|
|
|
|
|
pub mod artifacts;
|
|
|
|
|
|
|
|
pub use artifacts::{CompilerInput, CompilerOutput, EvmVersion};
|
2021-10-30 17:59:44 +00:00
|
|
|
use std::collections::btree_map::Entry;
|
2021-10-26 11:28:10 +00:00
|
|
|
|
|
|
|
pub mod cache;
|
|
|
|
|
|
|
|
mod compile;
|
2021-10-31 14:41:36 +00:00
|
|
|
pub use compile::*;
|
2021-10-26 11:28:10 +00:00
|
|
|
|
|
|
|
mod config;
|
2021-11-08 20:11:45 +00:00
|
|
|
pub use config::{AllowedLibPaths, ArtifactOutput, ProjectPathsConfig, SolcConfig};
|
2021-10-30 17:59:44 +00:00
|
|
|
|
|
|
|
use crate::{artifacts::Source, cache::SolFilesCache};
|
2021-10-26 11:28:10 +00:00
|
|
|
|
|
|
|
pub mod error;
|
|
|
|
pub mod utils;
|
2021-10-30 17:59:44 +00:00
|
|
|
use crate::artifacts::Sources;
|
2021-10-26 11:28:10 +00:00
|
|
|
use error::Result;
|
2021-10-30 17:59:44 +00:00
|
|
|
use std::{
|
|
|
|
collections::{BTreeMap, HashMap},
|
2021-11-08 20:11:45 +00:00
|
|
|
convert::TryInto,
|
2021-10-30 17:59:44 +00:00
|
|
|
fmt, fs, io,
|
|
|
|
path::PathBuf,
|
|
|
|
};
|
2021-10-26 11:28:10 +00:00
|
|
|
|
|
|
|
/// Handles contract compiling
|
|
|
|
#[derive(Debug)]
|
|
|
|
pub struct Project {
|
|
|
|
/// The layout of the
|
2021-10-30 17:59:44 +00:00
|
|
|
pub paths: ProjectPathsConfig,
|
2021-10-26 11:28:10 +00:00
|
|
|
/// Where to find solc
|
|
|
|
pub solc: Solc,
|
2021-10-30 17:59:44 +00:00
|
|
|
/// How solc invocation should be configured.
|
|
|
|
pub solc_config: SolcConfig,
|
2021-10-26 11:28:10 +00:00
|
|
|
/// Whether caching is enabled
|
|
|
|
pub cached: bool,
|
|
|
|
/// How to handle compiler output
|
|
|
|
pub artifacts: ArtifactOutput,
|
2021-10-30 17:59:44 +00:00
|
|
|
/// Errors/Warnings which match these error codes are not going to be logged
|
|
|
|
pub ignored_error_codes: Vec<u64>,
|
2021-11-08 20:11:45 +00:00
|
|
|
/// The paths which will be allowed for library inclusion
|
|
|
|
pub allowed_lib_paths: AllowedLibPaths,
|
2021-10-26 11:28:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Project {
|
2021-10-30 17:59:44 +00:00
|
|
|
/// Configure the current project
|
|
|
|
///
|
|
|
|
/// # Example
|
|
|
|
///
|
|
|
|
/// ```rust
|
|
|
|
/// use ethers_solc::Project;
|
|
|
|
/// let config = Project::builder().build().unwrap();
|
|
|
|
/// ```
|
|
|
|
pub fn builder() -> ProjectBuilder {
|
|
|
|
ProjectBuilder::default()
|
2021-10-26 11:28:10 +00:00
|
|
|
}
|
|
|
|
|
2021-10-30 17:59:44 +00:00
|
|
|
fn write_cache_file(&self, sources: Sources) -> Result<()> {
|
|
|
|
let cache = SolFilesCache::builder()
|
|
|
|
.root(&self.paths.root)
|
|
|
|
.solc_config(self.solc_config.clone())
|
|
|
|
.insert_files(sources)?;
|
|
|
|
if let Some(cache_dir) = self.paths.cache.parent() {
|
|
|
|
fs::create_dir_all(cache_dir)?
|
|
|
|
}
|
|
|
|
cache.write(&self.paths.cache)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns all sources found under the project's sources path
|
|
|
|
pub fn sources(&self) -> io::Result<Sources> {
|
|
|
|
Source::read_all_from(self.paths.sources.as_path())
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Attempts to read all unique libraries that are used as imports like "hardhat/console.sol"
|
|
|
|
fn resolved_libraries(
|
|
|
|
&self,
|
|
|
|
sources: &Sources,
|
|
|
|
) -> io::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()) {
|
|
|
|
entry.insert((Source::read(&lib)?, lib));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Ok(libs)
|
2021-10-26 11:28:10 +00:00
|
|
|
}
|
|
|
|
|
2021-10-30 17:59:44 +00:00
|
|
|
/// Attempts to compile the contracts found at the configured location.
|
|
|
|
///
|
|
|
|
/// NOTE: this does not check if the contracts were successfully compiled, see
|
|
|
|
/// `CompilerOutput::has_error` instead.
|
2021-11-03 08:05:09 +00:00
|
|
|
|
|
|
|
/// NB: If the `svm` feature is enabled, this function will automatically detect
|
|
|
|
/// solc versions across files.
|
2021-10-30 17:59:44 +00:00
|
|
|
pub fn compile(&self) -> Result<ProjectCompileOutput> {
|
2021-11-03 08:05:09 +00:00
|
|
|
let sources = self.sources()?;
|
|
|
|
|
|
|
|
#[cfg(not(all(feature = "svm", feature = "async")))]
|
|
|
|
{
|
|
|
|
self.compile_with_version(&self.solc, sources)
|
|
|
|
}
|
|
|
|
#[cfg(all(feature = "svm", feature = "async"))]
|
|
|
|
self.svm_compile(sources)
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(all(feature = "svm", feature = "async"))]
|
|
|
|
fn svm_compile(&self, sources: Sources) -> Result<ProjectCompileOutput> {
|
|
|
|
// split them by version
|
|
|
|
let mut sources_by_version = BTreeMap::new();
|
|
|
|
for (path, source) in sources.into_iter() {
|
|
|
|
// will detect and install the solc version
|
|
|
|
let version = Solc::detect_version(&source)?;
|
|
|
|
// gets the solc binary for that version, it is expected tha this will succeed
|
|
|
|
// AND find the solc since it was installed right above
|
2021-11-08 20:11:45 +00:00
|
|
|
let mut solc = Solc::find_svm_installed_version(version.to_string())?
|
2021-11-03 08:05:09 +00:00
|
|
|
.expect("solc should have been installed");
|
2021-11-08 20:11:45 +00:00
|
|
|
|
|
|
|
if !self.allowed_lib_paths.0.is_empty() {
|
|
|
|
solc = solc.arg("--allow-paths").arg(self.allowed_lib_paths.to_string());
|
|
|
|
}
|
2021-11-03 08:05:09 +00:00
|
|
|
let entry = sources_by_version.entry(solc).or_insert_with(BTreeMap::new);
|
|
|
|
entry.insert(path, source);
|
|
|
|
}
|
|
|
|
|
|
|
|
// run the compilation step for each version
|
|
|
|
let mut res = CompilerOutput::default();
|
|
|
|
for (solc, sources) in sources_by_version {
|
|
|
|
let output = self.compile_with_version(&solc, sources)?;
|
|
|
|
if let ProjectCompileOutput::Compiled((compiled, _)) = output {
|
|
|
|
res.errors.extend(compiled.errors);
|
|
|
|
res.sources.extend(compiled.sources);
|
|
|
|
res.contracts.extend(compiled.contracts);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Ok(if res.contracts.is_empty() {
|
|
|
|
ProjectCompileOutput::Unchanged
|
|
|
|
} else {
|
|
|
|
ProjectCompileOutput::Compiled((res, &self.ignored_error_codes))
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn compile_with_version(
|
|
|
|
&self,
|
|
|
|
solc: &Solc,
|
|
|
|
mut sources: Sources,
|
|
|
|
) -> Result<ProjectCompileOutput> {
|
2021-10-30 17:59:44 +00:00
|
|
|
// add all libraries to the source set while keeping track of their actual disk path
|
|
|
|
let mut source_name_path = HashMap::new();
|
|
|
|
let mut path_source_name = HashMap::new();
|
|
|
|
for (import, (source, path)) in self.resolved_libraries(&sources)? {
|
|
|
|
// inserting with absolute path here and keep track of the source name <-> path mappings
|
|
|
|
sources.insert(path.clone(), source);
|
|
|
|
path_source_name.insert(path.clone(), import.clone());
|
|
|
|
source_name_path.insert(import, path);
|
|
|
|
}
|
|
|
|
|
2021-10-30 18:27:17 +00:00
|
|
|
// If there's a cache set, filter to only re-compile the files which were changed
|
|
|
|
let sources = if self.cached && self.paths.cache.exists() {
|
2021-10-30 17:59:44 +00:00
|
|
|
let cache = SolFilesCache::read(&self.paths.cache)?;
|
2021-10-30 18:27:17 +00:00
|
|
|
let changed_files = cache.get_changed_files(sources, Some(&self.solc_config));
|
|
|
|
if changed_files.is_empty() {
|
2021-10-30 17:59:44 +00:00
|
|
|
return Ok(ProjectCompileOutput::Unchanged)
|
|
|
|
}
|
2021-10-30 18:27:17 +00:00
|
|
|
changed_files
|
|
|
|
} else {
|
|
|
|
sources
|
|
|
|
};
|
2021-10-30 17:59:44 +00:00
|
|
|
|
|
|
|
// replace absolute path with source name to make solc happy
|
|
|
|
let sources = apply_mappings(sources, path_source_name);
|
|
|
|
|
2021-11-03 08:05:09 +00:00
|
|
|
let input = CompilerInput::with_sources(sources).normalize_evm_version(&solc.version()?);
|
|
|
|
let output = solc.compile(&input)?;
|
2021-10-30 17:59:44 +00:00
|
|
|
if output.has_error() {
|
|
|
|
return Ok(ProjectCompileOutput::Compiled((output, &self.ignored_error_codes)))
|
|
|
|
}
|
|
|
|
|
2021-10-26 11:28:10 +00:00
|
|
|
if self.cached {
|
2021-10-30 17:59:44 +00:00
|
|
|
// reapply to disk paths
|
|
|
|
let sources = apply_mappings(input.sources, source_name_path);
|
|
|
|
// create cache file
|
|
|
|
self.write_cache_file(sources)?;
|
|
|
|
}
|
|
|
|
|
|
|
|
self.artifacts.on_output(&output, &self.paths)?;
|
|
|
|
Ok(ProjectCompileOutput::Compiled((output, &self.ignored_error_codes)))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn apply_mappings(sources: Sources, mut mappings: HashMap<PathBuf, PathBuf>) -> Sources {
|
|
|
|
sources
|
|
|
|
.into_iter()
|
|
|
|
.map(|(import, source)| {
|
|
|
|
if let Some(path) = mappings.remove(&import) {
|
|
|
|
(path, source)
|
2021-10-26 11:28:10 +00:00
|
|
|
} else {
|
2021-10-30 17:59:44 +00:00
|
|
|
(import, source)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.collect()
|
|
|
|
}
|
|
|
|
|
|
|
|
pub struct ProjectBuilder {
|
|
|
|
/// The layout of the
|
|
|
|
paths: Option<ProjectPathsConfig>,
|
|
|
|
/// Where to find solc
|
|
|
|
solc: Option<Solc>,
|
|
|
|
/// How solc invocation should be configured.
|
|
|
|
solc_config: Option<SolcConfig>,
|
|
|
|
/// Whether caching is enabled, default is true.
|
|
|
|
cached: bool,
|
|
|
|
/// How to handle compiler output
|
|
|
|
artifacts: Option<ArtifactOutput>,
|
|
|
|
/// Which error codes to ignore
|
|
|
|
pub ignored_error_codes: Vec<u64>,
|
2021-11-08 20:11:45 +00:00
|
|
|
/// All allowed paths
|
|
|
|
pub allowed_paths: Vec<PathBuf>,
|
2021-10-30 17:59:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl ProjectBuilder {
|
|
|
|
pub fn paths(mut self, paths: ProjectPathsConfig) -> Self {
|
|
|
|
self.paths = Some(paths);
|
|
|
|
self
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn solc(mut self, solc: impl Into<Solc>) -> Self {
|
|
|
|
self.solc = Some(solc.into());
|
|
|
|
self
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn solc_config(mut self, solc_config: SolcConfig) -> Self {
|
|
|
|
self.solc_config = Some(solc_config);
|
|
|
|
self
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn artifacts(mut self, artifacts: ArtifactOutput) -> Self {
|
|
|
|
self.artifacts = Some(artifacts);
|
|
|
|
self
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn ignore_error_code(mut self, code: u64) -> Self {
|
|
|
|
self.ignored_error_codes.push(code);
|
|
|
|
self
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Disables cached builds
|
|
|
|
pub fn ephemeral(mut self) -> Self {
|
|
|
|
self.cached = false;
|
|
|
|
self
|
|
|
|
}
|
|
|
|
|
2021-11-08 20:11:45 +00:00
|
|
|
/// Adds an allowed-path to the solc executable
|
|
|
|
pub fn allowed_path<T: Into<PathBuf>>(mut self, path: T) -> Self {
|
|
|
|
self.allowed_paths.push(path.into());
|
|
|
|
self
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Adds multiple allowed-path to the solc executable
|
|
|
|
pub fn allowed_paths<I, S>(mut self, args: I) -> Self
|
|
|
|
where
|
|
|
|
I: IntoIterator<Item = S>,
|
|
|
|
S: Into<PathBuf>,
|
|
|
|
{
|
|
|
|
for arg in args {
|
|
|
|
self = self.allowed_path(arg);
|
|
|
|
}
|
|
|
|
self
|
|
|
|
}
|
|
|
|
|
2021-10-30 17:59:44 +00:00
|
|
|
pub fn build(self) -> Result<Project> {
|
2021-11-08 20:11:45 +00:00
|
|
|
let Self {
|
|
|
|
paths,
|
|
|
|
solc,
|
|
|
|
solc_config,
|
|
|
|
cached,
|
|
|
|
artifacts,
|
|
|
|
ignored_error_codes,
|
|
|
|
mut allowed_paths,
|
|
|
|
} = self;
|
2021-10-30 17:59:44 +00:00
|
|
|
|
|
|
|
let solc = solc.unwrap_or_default();
|
|
|
|
let solc_config = solc_config.map(Ok).unwrap_or_else(|| {
|
|
|
|
let version = solc.version()?;
|
|
|
|
SolcConfig::builder().version(version.to_string()).build()
|
|
|
|
})?;
|
|
|
|
|
2021-11-08 20:11:45 +00:00
|
|
|
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())
|
|
|
|
}
|
|
|
|
|
2021-10-30 17:59:44 +00:00
|
|
|
Ok(Project {
|
2021-11-08 20:11:45 +00:00
|
|
|
paths,
|
2021-10-30 17:59:44 +00:00
|
|
|
solc,
|
|
|
|
solc_config,
|
|
|
|
cached,
|
|
|
|
artifacts: artifacts.unwrap_or_default(),
|
|
|
|
ignored_error_codes,
|
2021-11-08 20:11:45 +00:00
|
|
|
allowed_lib_paths: allowed_paths.try_into()?,
|
2021-10-30 17:59:44 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Default for ProjectBuilder {
|
|
|
|
fn default() -> Self {
|
|
|
|
Self {
|
|
|
|
paths: None,
|
|
|
|
solc: None,
|
|
|
|
solc_config: None,
|
|
|
|
cached: true,
|
|
|
|
artifacts: None,
|
|
|
|
ignored_error_codes: Vec::new(),
|
2021-11-08 20:11:45 +00:00
|
|
|
allowed_paths: vec![],
|
2021-10-26 11:28:10 +00:00
|
|
|
}
|
2021-10-30 17:59:44 +00:00
|
|
|
}
|
|
|
|
}
|
2021-10-26 11:28:10 +00:00
|
|
|
|
2021-10-31 11:34:51 +00:00
|
|
|
#[derive(Debug, Clone, PartialEq)]
|
2021-10-30 17:59:44 +00:00
|
|
|
pub enum ProjectCompileOutput<'a> {
|
|
|
|
/// Nothing to compile because unchanged sources
|
|
|
|
Unchanged,
|
|
|
|
Compiled((CompilerOutput, &'a [u64])),
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a> fmt::Display for ProjectCompileOutput<'a> {
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
|
|
match self {
|
|
|
|
ProjectCompileOutput::Unchanged => f.write_str("Nothing to compile"),
|
|
|
|
ProjectCompileOutput::Compiled((output, ignored_error_codes)) => {
|
|
|
|
output.diagnostics(ignored_error_codes).fmt(f)
|
|
|
|
}
|
|
|
|
}
|
2021-10-26 11:28:10 +00:00
|
|
|
}
|
|
|
|
}
|
2021-11-03 08:05:09 +00:00
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
#[cfg(all(feature = "svm", feature = "async"))]
|
|
|
|
fn test_build_all_versions() {
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
let paths = ProjectPathsConfig::builder()
|
|
|
|
.root("./test-data/test-contract-versions")
|
|
|
|
.sources("./test-data/test-contract-versions")
|
|
|
|
.build()
|
|
|
|
.unwrap();
|
|
|
|
let project = Project::builder()
|
|
|
|
.paths(paths)
|
|
|
|
.ephemeral()
|
|
|
|
.artifacts(ArtifactOutput::Nothing)
|
|
|
|
.build()
|
|
|
|
.unwrap();
|
|
|
|
let compiled = project.compile().unwrap();
|
|
|
|
let contracts = match compiled {
|
|
|
|
ProjectCompileOutput::Compiled((out, _)) => {
|
|
|
|
assert!(!out.has_error());
|
|
|
|
out.contracts
|
|
|
|
}
|
|
|
|
_ => panic!("must compile"),
|
|
|
|
};
|
|
|
|
// Contracts A to F
|
|
|
|
assert_eq!(contracts.keys().count(), 5);
|
|
|
|
}
|
2021-11-08 20:11:45 +00:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
#[cfg(all(feature = "svm", feature = "async"))]
|
|
|
|
fn test_build_many_libs() {
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
let root = std::fs::canonicalize("./test-data/test-contract-libs").unwrap();
|
|
|
|
|
|
|
|
let paths = ProjectPathsConfig::builder()
|
|
|
|
.root(&root)
|
|
|
|
.sources(root.join("src"))
|
|
|
|
.lib(root.join("lib1"))
|
|
|
|
.lib(root.join("lib2"))
|
|
|
|
.build()
|
|
|
|
.unwrap();
|
|
|
|
let project = Project::builder()
|
|
|
|
.paths(paths)
|
|
|
|
.ephemeral()
|
|
|
|
.artifacts(ArtifactOutput::Nothing)
|
|
|
|
.build()
|
|
|
|
.unwrap();
|
|
|
|
let compiled = project.compile().unwrap();
|
|
|
|
let contracts = match compiled {
|
|
|
|
ProjectCompileOutput::Compiled((out, _)) => {
|
|
|
|
assert!(!out.has_error());
|
|
|
|
out.contracts
|
|
|
|
}
|
|
|
|
_ => panic!("must compile"),
|
|
|
|
};
|
|
|
|
assert_eq!(contracts.keys().count(), 3);
|
|
|
|
}
|
2021-11-03 08:05:09 +00:00
|
|
|
}
|