#![doc = include_str ! ("../README.md")] pub mod artifacts; pub mod sourcemap; pub use artifacts::{CompilerInput, CompilerOutput, EvmVersion}; use std::collections::btree_map::Entry; pub mod cache; pub mod hh; 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 mod remappings; use crate::{artifacts::Source, cache::SolFilesCache}; pub mod error; pub mod utils; use crate::{ artifacts::Sources, cache::PathMap, error::{SolcError, SolcIoError}, }; use error::Result; use std::{ borrow::Cow, collections::BTreeMap, convert::TryInto, fmt, fs, marker::PhantomData, path::{Path, PathBuf}, }; /// Utilities for creating, mocking and testing of (temporary) projects #[cfg(feature = "project-util")] pub mod project_util; /// Represents a project workspace and handles `solc` compiling of all contracts in that workspace. #[derive(Debug)] pub struct Project { /// The layout of the pub paths: ProjectPathsConfig, /// Where to find solc pub solc: Solc, /// How solc invocation should be configured. pub solc_config: SolcConfig, /// Whether caching is enabled pub cached: bool, /// Whether writing artifacts to disk is enabled pub no_artifacts: bool, /// Whether writing artifacts to disk is enabled pub auto_detect: bool, /// How to handle compiler output pub artifacts: PhantomData, /// Errors/Warnings which match these error codes are not going to be logged pub ignored_error_codes: Vec, /// The paths which will be allowed for library inclusion pub allowed_lib_paths: AllowedLibPaths, /// Maximum number of `solc` processes to run simultaneously. solc_jobs: usize, } impl Project { /// Convenience function to call `ProjectBuilder::default()` /// /// # Example /// /// Configure with `MinimalCombinedArtifacts` artifacts output /// /// ```rust /// use ethers_solc::Project; /// let config = Project::builder().build().unwrap(); /// ``` /// /// To configure any a project with any `ArtifactOutput` use either /// /// ```rust /// use ethers_solc::Project; /// let config = Project::builder().build().unwrap(); /// ``` /// /// or use the builder directly /// /// ```rust /// use ethers_solc::{MinimalCombinedArtifacts, ProjectBuilder}; /// let config = ProjectBuilder::::default().build().unwrap(); /// ``` pub fn builder() -> ProjectBuilder { ProjectBuilder::default() } } impl Project { /// 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 } /// Sets the maximum number of parallel `solc` processes to run simultaneously. 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)>, ) -> 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 { self.paths.read_sources() } /// This emits the cargo [`rerun-if-changed`](https://doc.rust-lang.org/cargo/reference/build-scripts.html#cargorerun-if-changedpath) instruction. /// Which tells Cargo to re-run the build script if a file inside the project's sources /// directory has changed. /// /// Use this if you compile a project in a `build.rs` file. /// /// # Example `build.rs` file /// /// /// ```no_run /// use ethers_solc::{Project, ProjectPathsConfig}; /// // configure the project with all its paths, solc, cache etc. where the root dir is the current rust project. /// let project = Project::builder() /// .paths(ProjectPathsConfig::hardhat(env!("CARGO_MANIFEST_DIR")).unwrap()) /// .build() /// .unwrap(); /// let output = project.compile().unwrap(); /// // Tell Cargo that if a source file changes, to rerun this build script. /// project.rerun_if_sources_changed(); /// ``` pub fn rerun_if_sources_changed(&self) { 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> { 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. /// /// # Example /// /// ``` /// use ethers_solc::Project; /// # fn demo(project: Project) { /// let project = Project::builder().build().unwrap(); /// let output = project.compile().unwrap(); /// # } /// ``` #[tracing::instrument(skip_all, name = "compile")] pub fn compile(&self) -> Result> { let sources = self.paths.read_input_files()?; tracing::trace!("found {} sources to compile: {:?}", sources.len(), sources.keys()); #[cfg(all(feature = "svm", feature = "async"))] if self.auto_detect { tracing::trace!("using solc auto detection to compile sources"); 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 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> { 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>, ) -> Result> { 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. /// /// This runs `Self::solc_jobs` parallel `solc` jobs at most. #[cfg(all(feature = "svm", feature = "async"))] fn compile_many( &self, sources_by_version: BTreeMap>, ) -> Result> { 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) } /// Compiles the given source files with the exact `Solc` executable /// /// First all libraries for the sources are resolved by scanning all their imports. /// If caching is enabled for the `Project`, then all unchanged files are filtered from the /// sources and their existing artifacts are read instead. This will also update the cache /// file and cleans up entries for files which may have been removed. Unchanged files that /// for which an artifact exist, are not compiled again. /// /// # Example /// /// ``` /// use ethers_solc::{Project, Solc}; /// # fn demo(project: Project) { /// let project = Project::builder().build().unwrap(); /// let sources = project.paths.read_sources().unwrap(); /// project /// .compile_with_version( /// &Solc::find_svm_installed_version("0.8.11").unwrap().unwrap(), /// sources, /// ) /// .unwrap(); /// # } /// ``` pub fn compile_with_version( &self, solc: &Solc, sources: Sources, ) -> Result> { 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> { 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::( 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::(&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)) } /// 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. pub fn cleanup(&self) -> std::result::Result<(), SolcIoError> { tracing::trace!("clean up project"); if self.cache_path().exists() { std::fs::remove_file(self.cache_path()) .map_err(|err| SolcIoError::new(err, self.cache_path()))?; if let Some(cache_folder) = self.cache_path().parent() { // remove the cache folder if the cache file was the only file if cache_folder .read_dir() .map_err(|err| SolcIoError::new(err, cache_folder))? .next() .is_none() { std::fs::remove_dir(cache_folder) .map_err(|err| SolcIoError::new(err, cache_folder))?; } } tracing::trace!("removed cache file \"{}\"", self.cache_path().display()); } if self.paths.artifacts.exists() { std::fs::remove_dir_all(self.artifacts_path()) .map_err(|err| SolcIoError::new(err, self.artifacts_path().clone()))?; tracing::trace!("removed artifacts dir \"{}\"", self.artifacts_path().display()); } Ok(()) } /// Flattens the target 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. /// /// NOTE: 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 { self.paths.flatten(target) } } enum PreprocessedJob { Unchanged(BTreeMap), Items(Sources, PathMap, BTreeMap), } pub struct ProjectBuilder { /// The layout of the paths: Option, /// Where to find solc solc: Option, /// How solc invocation should be configured. solc_config: Option, /// Whether caching is enabled, default is true. cached: bool, /// Whether writing artifacts to disk is enabled, default is true. no_artifacts: bool, /// Whether automatic solc version detection is enabled auto_detect: bool, artifacts: PhantomData, /// Which error codes to ignore pub ignored_error_codes: Vec, /// All allowed paths pub allowed_paths: Vec, solc_jobs: Option, } impl ProjectBuilder { #[must_use] pub fn paths(mut self, paths: ProjectPathsConfig) -> Self { self.paths = Some(paths); self } #[must_use] pub fn solc(mut self, solc: impl Into) -> Self { self.solc = Some(solc.into()); self } #[must_use] pub fn solc_config(mut self, solc_config: SolcConfig) -> Self { self.solc_config = Some(solc_config); self } #[must_use] pub fn ignore_error_code(mut self, code: u64) -> Self { self.ignored_error_codes.push(code); self } #[must_use] pub fn ignore_error_codes(mut self, codes: impl IntoIterator) -> Self { for code in codes { self = self.ignore_error_code(code); } self } /// Disables cached builds #[must_use] pub fn ephemeral(self) -> Self { self.set_cached(false) } /// Sets the cache status #[must_use] pub fn set_cached(mut self, cached: bool) -> Self { self.cached = cached; self } /// Disables writing artifacts to disk #[must_use] pub fn no_artifacts(self) -> Self { self.set_no_artifacts(true) } /// Sets the no artifacts status #[must_use] pub fn set_no_artifacts(mut self, artifacts: bool) -> Self { self.no_artifacts = artifacts; self } /// Sets automatic solc version detection #[must_use] pub fn set_auto_detect(mut self, auto_detect: bool) -> Self { self.auto_detect = auto_detect; self } /// Disables automatic solc version detection #[must_use] pub fn no_auto_detect(self) -> Self { self.set_auto_detect(false) } /// Sets the maximum number of parallel `solc` processes to run simultaneously. /// /// # Panics /// /// `jobs` must be at least 1 #[must_use] pub fn solc_jobs(mut self, jobs: usize) -> Self { assert!(jobs > 0); self.solc_jobs = Some(jobs); self } /// Sets the number of parallel `solc` processes to `1`, no parallelization #[must_use] pub fn single_solc_jobs(self) -> Self { self.solc_jobs(1) } /// Set arbitrary `ArtifactOutputHandler` pub fn artifacts(self) -> ProjectBuilder { let ProjectBuilder { paths, solc, solc_config, cached, no_artifacts, auto_detect, ignored_error_codes, allowed_paths, solc_jobs, .. } = self; ProjectBuilder { paths, solc, solc_config, cached, no_artifacts, auto_detect, artifacts: PhantomData::default(), ignored_error_codes, allowed_paths, solc_jobs, } } /// Adds an allowed-path to the solc executable #[must_use] pub fn allowed_path>(mut self, path: T) -> Self { self.allowed_paths.push(path.into()); self } /// Adds multiple allowed-path to the solc executable #[must_use] pub fn allowed_paths(mut self, args: I) -> Self where I: IntoIterator, S: Into, { for arg in args { self = self.allowed_path(arg); } self } pub fn build(self) -> Result> { let Self { paths, solc, solc_config, cached, no_artifacts, auto_detect, artifacts, ignored_error_codes, mut allowed_paths, solc_jobs, } = self; 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()) } Ok(Project { paths, solc, solc_config, cached, no_artifacts, auto_detect, artifacts, ignored_error_codes, allowed_lib_paths: allowed_paths.try_into()?, solc_jobs: solc_jobs.unwrap_or_else(::num_cpus::get), }) } } impl Default for ProjectBuilder { fn default() -> Self { Self { paths: None, solc: None, solc_config: None, cached: true, no_artifacts: false, auto_detect: true, artifacts: PhantomData::default(), ignored_error_codes: Vec::new(), allowed_paths: vec![], solc_jobs: None, } } } /// The outcome of `Project::compile` #[derive(Debug, Clone, PartialEq, Default)] pub struct ProjectCompileOutput { /// 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, /// All artifacts that were read from cache artifacts: BTreeMap, ignored_error_codes: Vec, } impl ProjectCompileOutput { pub fn with_ignored_errors(ignored_errors: Vec) -> Self { Self { compiler_output: None, artifacts: Default::default(), ignored_error_codes: ignored_errors, } } pub fn from_unchanged(artifacts: BTreeMap) -> Self { Self { compiler_output: None, artifacts, ignored_error_codes: vec![] } } pub fn from_compiler_output( compiler_output: CompilerOutput, ignored_error_codes: Vec, ) -> 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, ignored_error_codes: Vec, ) -> 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 = /// 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) { 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) { 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) -> Option { 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 ProjectCompileOutput where T::Artifact: Clone, { /// Finds the first contract with the given name pub fn find(&self, contract_name: impl AsRef) -> Option> { 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 ProjectCompileOutput { /// All artifacts together with their contract file name and name `:` /// /// # Example /// /// ```no_run /// use std::collections::BTreeMap; /// use ethers_solc::artifacts::CompactContract; /// use ethers_solc::Project; /// /// let project = Project::builder().build().unwrap(); /// let contracts: BTreeMap = project.compile().unwrap().into_artifacts().collect(); /// ``` pub fn into_artifacts(mut self) -> Box> { 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> = 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 fmt::Display for ProjectCompileOutput { 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") } } } #[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).no_artifacts().ephemeral().build().unwrap(); let compiled = project.compile().unwrap(); assert!(!compiled.has_compiler_errors()); let contracts = compiled.output().contracts; // Contracts A to F assert_eq!(contracts.keys().count(), 5); } #[test] #[cfg(all(feature = "svm", feature = "async"))] fn test_build_many_libs() { use super::*; let root = utils::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) .no_artifacts() .ephemeral() .no_artifacts() .build() .unwrap(); let compiled = project.compile().unwrap(); assert!(!compiled.has_compiler_errors()); let contracts = compiled.output().contracts; assert_eq!(contracts.keys().count(), 3); } #[test] #[cfg(all(feature = "svm", feature = "async"))] fn test_build_remappings() { use super::*; let root = utils::canonicalize("./test-data/test-contract-remappings").unwrap(); let paths = ProjectPathsConfig::builder() .root(&root) .sources(root.join("src")) .lib(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); } }