#![doc = include_str!("../README.md")]
#![cfg_attr(not(test), warn(unused_crate_dependencies))]
#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
#[macro_use]
extern crate tracing;
#[cfg(feature = "project-util")]
#[macro_use]
extern crate foundry_compilers_core;
mod artifact_output;
pub use artifact_output::*;
pub mod buildinfo;
pub mod cache;
pub mod flatten;
pub mod resolver;
pub use resolver::Graph;
pub mod compilers;
pub use compilers::*;
mod compile;
pub use compile::{
output::{AggregatedCompilerOutput, ProjectCompileOutput},
*,
};
mod config;
pub use config::{PathStyle, ProjectPaths, ProjectPathsConfig, SolcConfig};
mod filter;
pub use filter::{FileFilter, SparseOutputFilter, TestFileFilter};
pub mod report;
#[cfg(feature = "project-util")]
pub mod project_util;
pub use foundry_compilers_artifacts as artifacts;
pub use foundry_compilers_core::{error, utils};
use cache::CompilerCache;
use compile::output::contracts::VersionedContracts;
use compilers::multi::MultiCompiler;
use derivative::Derivative;
use foundry_compilers_artifacts::solc::{
output_selection::OutputSelection,
sources::{Source, SourceCompilationKind, Sources},
Contract, Severity, SourceFile, StandardJsonCompilerInput,
};
use foundry_compilers_core::error::{Result, SolcError, SolcIoError};
use output::sources::{VersionedSourceFile, VersionedSourceFiles};
use project::ProjectCompiler;
use semver::Version;
use solang_parser::pt::SourceUnitPart;
use solc::SolcSettings;
use std::{
collections::{BTreeMap, HashMap, HashSet},
fs,
path::{Path, PathBuf},
};
#[derive(Clone, Derivative)]
#[derivative(Debug)]
pub struct Project<C: Compiler = MultiCompiler, T: ArtifactOutput = ConfigurableArtifacts> {
pub compiler: C,
pub locked_versions: HashMap<C::Language, Version>,
pub paths: ProjectPathsConfig<C::Language>,
pub settings: C::Settings,
pub cached: bool,
pub build_info: bool,
pub no_artifacts: bool,
pub artifacts: T,
pub ignored_error_codes: Vec<u64>,
pub ignored_file_paths: Vec<PathBuf>,
pub compiler_severity_filter: Severity,
solc_jobs: usize,
pub offline: bool,
pub slash_paths: bool,
#[derivative(Debug = "ignore")]
pub sparse_output: Option<Box<dyn FileFilter>>,
}
impl Project {
pub fn builder() -> ProjectBuilder {
ProjectBuilder::default()
}
}
impl<T: ArtifactOutput, C: Compiler> Project<C, T> {
pub fn artifacts_handler(&self) -> &T {
&self.artifacts
}
}
impl<C: Compiler, T: ArtifactOutput> Project<C, T>
where
C::Settings: Into<SolcSettings>,
{
pub fn standard_json_input(&self, target: &Path) -> Result<StandardJsonCompilerInput> {
trace!(?target, "Building standard-json-input");
let graph = Graph::<C::ParsedSource>::resolve(&self.paths)?;
let target_index = graph.files().get(target).ok_or_else(|| {
SolcError::msg(format!("cannot resolve file at {:?}", target.display()))
})?;
let mut sources = Vec::new();
let mut unique_paths = HashSet::new();
let (path, source) = graph.node(*target_index).unpack();
unique_paths.insert(path.clone());
sources.push((path, source));
sources.extend(
graph
.all_imported_nodes(*target_index)
.map(|index| graph.node(index).unpack())
.filter(|(p, _)| unique_paths.insert(p.to_path_buf())),
);
let root = self.root();
let sources = sources
.into_iter()
.map(|(path, source)| (rebase_path(root, path), source.clone()))
.collect();
let mut settings = self.settings.clone().into();
settings.remappings = self
.paths
.remappings
.clone()
.into_iter()
.map(|r| r.into_relative(self.root()).to_relative_remapping())
.collect::<Vec<_>>();
let input = StandardJsonCompilerInput::new(sources, settings.settings);
Ok(input)
}
}
impl<T: ArtifactOutput, C: Compiler> Project<C, T> {
pub fn artifacts_path(&self) -> &PathBuf {
&self.paths.artifacts
}
pub fn sources_path(&self) -> &PathBuf {
&self.paths.sources
}
pub fn cache_path(&self) -> &PathBuf {
&self.paths.cache
}
pub fn build_info_path(&self) -> &PathBuf {
&self.paths.build_infos
}
pub fn root(&self) -> &PathBuf {
&self.paths.root
}
pub fn read_cache_file(&self) -> Result<CompilerCache<C::Settings>> {
CompilerCache::read_joined(&self.paths)
}
pub fn set_solc_jobs(&mut self, jobs: usize) {
assert!(jobs > 0);
self.solc_jobs = jobs;
}
#[instrument(skip_all, fields(name = "sources"))]
pub fn sources(&self) -> Result<Sources> {
self.paths.read_sources()
}
pub fn rerun_if_sources_changed(&self) {
println!("cargo:rerun-if-changed={}", self.paths.sources.display())
}
pub fn compile(&self) -> Result<ProjectCompileOutput<C, T>> {
project::ProjectCompiler::new(self)?.compile()
}
pub fn compile_file(&self, file: impl Into<PathBuf>) -> Result<ProjectCompileOutput<C, T>> {
let file = file.into();
let source = Source::read(&file)?;
project::ProjectCompiler::with_sources(self, Sources::from([(file, source)]))?.compile()
}
pub fn compile_files<P, I>(&self, files: I) -> Result<ProjectCompileOutput<C, T>>
where
I: IntoIterator<Item = P>,
P: Into<PathBuf>,
{
let sources = Source::read_all(files)?;
ProjectCompiler::with_sources(self, sources)?.compile()
}
pub fn cleanup(&self) -> std::result::Result<(), SolcIoError> {
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().filter(|cache_folder| self.root() != cache_folder)
{
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))?;
}
}
trace!("removed cache file \"{}\"", self.cache_path().display());
}
if self.artifacts_path().exists() && self.root() != self.artifacts_path() {
std::fs::remove_dir_all(self.artifacts_path())
.map_err(|err| SolcIoError::new(err, self.artifacts_path().clone()))?;
trace!("removed artifacts dir \"{}\"", self.artifacts_path().display());
}
if self.build_info_path().exists() && self.root() != self.build_info_path() {
std::fs::remove_dir_all(self.build_info_path())
.map_err(|err| SolcIoError::new(err, self.build_info_path().clone()))?;
tracing::trace!("removed build-info dir \"{}\"", self.build_info_path().display());
}
Ok(())
}
fn collect_contract_names_solc(&self) -> Result<HashMap<String, Vec<PathBuf>>>
where
T: Clone,
C: Clone,
{
let mut temp_project = (*self).clone();
temp_project.no_artifacts = true;
temp_project.settings.update_output_selection(|selection| {
*selection = OutputSelection::common_output_selection(["abi".to_string()]);
});
let output = temp_project.compile()?;
if output.has_compiler_errors() {
return Err(SolcError::msg(output));
}
let contracts = output.into_artifacts().fold(
HashMap::new(),
|mut contracts: HashMap<_, Vec<_>>, (id, _)| {
contracts.entry(id.name).or_default().push(id.source);
contracts
},
);
Ok(contracts)
}
fn collect_contract_names(&self) -> Result<HashMap<String, Vec<PathBuf>>>
where
T: Clone,
C: Clone,
{
let graph = Graph::<C::ParsedSource>::resolve(&self.paths)?;
let mut contracts: HashMap<String, Vec<PathBuf>> = HashMap::new();
for file in graph.files().keys() {
let src = fs::read_to_string(file).map_err(|e| SolcError::io(e, file))?;
let Ok((parsed, _)) = solang_parser::parse(&src, 0) else {
return self.collect_contract_names_solc();
};
for part in parsed.0 {
if let SourceUnitPart::ContractDefinition(contract) = part {
if let Some(name) = contract.name {
contracts.entry(name.name).or_default().push(file.clone());
}
}
}
}
Ok(contracts)
}
pub fn find_contract_path(&self, target_name: &str) -> Result<PathBuf>
where
T: Clone,
C: Clone,
{
let mut contracts = self.collect_contract_names()?;
if contracts.get(target_name).map_or(true, |paths| paths.is_empty()) {
return Err(SolcError::msg(format!("No contract found with the name `{target_name}`")));
}
let mut paths = contracts.remove(target_name).unwrap();
if paths.len() > 1 {
return Err(SolcError::msg(format!(
"Multiple contracts found with the name `{target_name}`"
)));
}
Ok(paths.remove(0))
}
}
pub struct ProjectBuilder<C: Compiler = MultiCompiler, T: ArtifactOutput = ConfigurableArtifacts> {
paths: Option<ProjectPathsConfig<C::Language>>,
locked_versions: HashMap<C::Language, Version>,
settings: Option<C::Settings>,
cached: bool,
build_info: bool,
no_artifacts: bool,
offline: bool,
slash_paths: bool,
artifacts: T,
pub ignored_error_codes: Vec<u64>,
pub ignored_file_paths: Vec<PathBuf>,
compiler_severity_filter: Severity,
solc_jobs: Option<usize>,
sparse_output: Option<Box<dyn FileFilter>>,
}
impl<C: Compiler, T: ArtifactOutput> ProjectBuilder<C, T> {
pub fn new(artifacts: T) -> Self {
Self {
paths: None,
cached: true,
build_info: false,
no_artifacts: false,
offline: false,
slash_paths: true,
artifacts,
ignored_error_codes: Vec::new(),
ignored_file_paths: Vec::new(),
compiler_severity_filter: Severity::Error,
solc_jobs: None,
settings: None,
locked_versions: Default::default(),
sparse_output: None,
}
}
#[must_use]
pub fn paths(mut self, paths: ProjectPathsConfig<C::Language>) -> Self {
self.paths = Some(paths);
self
}
#[must_use]
pub fn settings(mut self, settings: C::Settings) -> Self {
self.settings = Some(settings);
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<Item = u64>) -> Self {
for code in codes {
self = self.ignore_error_code(code);
}
self
}
pub fn ignore_paths(mut self, paths: Vec<PathBuf>) -> Self {
self.ignored_file_paths = paths;
self
}
#[must_use]
pub fn set_compiler_severity_filter(mut self, compiler_severity_filter: Severity) -> Self {
self.compiler_severity_filter = compiler_severity_filter;
self
}
#[must_use]
pub fn ephemeral(self) -> Self {
self.set_cached(false)
}
#[must_use]
pub fn set_cached(mut self, cached: bool) -> Self {
self.cached = cached;
self
}
#[must_use]
pub fn set_build_info(mut self, build_info: bool) -> Self {
self.build_info = build_info;
self
}
#[must_use]
pub fn offline(self) -> Self {
self.set_offline(true)
}
#[must_use]
pub fn set_offline(mut self, offline: bool) -> Self {
self.offline = offline;
self
}
#[must_use]
pub fn set_slashed_paths(mut self, slashed_paths: bool) -> Self {
self.slash_paths = slashed_paths;
self
}
#[must_use]
pub fn no_artifacts(self) -> Self {
self.set_no_artifacts(true)
}
#[must_use]
pub fn set_no_artifacts(mut self, artifacts: bool) -> Self {
self.no_artifacts = artifacts;
self
}
#[must_use]
pub fn solc_jobs(mut self, jobs: usize) -> Self {
assert!(jobs > 0);
self.solc_jobs = Some(jobs);
self
}
#[must_use]
pub fn single_solc_jobs(self) -> Self {
self.solc_jobs(1)
}
#[must_use]
pub fn locked_version(mut self, lang: impl Into<C::Language>, version: Version) -> Self {
self.locked_versions.insert(lang.into(), version);
self
}
#[must_use]
pub fn locked_versions(mut self, versions: HashMap<C::Language, Version>) -> Self {
self.locked_versions = versions;
self
}
#[must_use]
pub fn sparse_output<F>(mut self, filter: F) -> Self
where
F: FileFilter + 'static,
{
self.sparse_output = Some(Box::new(filter));
self
}
pub fn artifacts<A: ArtifactOutput>(self, artifacts: A) -> ProjectBuilder<C, A> {
let Self {
paths,
cached,
no_artifacts,
ignored_error_codes,
compiler_severity_filter,
solc_jobs,
offline,
build_info,
slash_paths,
ignored_file_paths,
settings,
locked_versions,
sparse_output,
..
} = self;
ProjectBuilder {
paths,
cached,
no_artifacts,
offline,
slash_paths,
artifacts,
ignored_error_codes,
ignored_file_paths,
compiler_severity_filter,
solc_jobs,
build_info,
settings,
locked_versions,
sparse_output,
}
}
pub fn build(self, compiler: C) -> Result<Project<C, T>> {
let Self {
paths,
cached,
no_artifacts,
artifacts,
ignored_error_codes,
ignored_file_paths,
compiler_severity_filter,
solc_jobs,
offline,
build_info,
slash_paths,
settings,
locked_versions,
sparse_output,
} = self;
let mut paths = paths.map(Ok).unwrap_or_else(ProjectPathsConfig::current_hardhat)?;
if slash_paths {
paths.slash_paths();
}
Ok(Project {
compiler,
paths,
cached,
build_info,
no_artifacts,
artifacts,
ignored_error_codes,
ignored_file_paths,
compiler_severity_filter,
solc_jobs: solc_jobs
.or_else(|| std::thread::available_parallelism().ok().map(|n| n.get()))
.unwrap_or(1),
offline,
slash_paths,
settings: settings.unwrap_or_default(),
locked_versions,
sparse_output,
})
}
}
impl<C: Compiler, T: ArtifactOutput + Default> Default for ProjectBuilder<C, T> {
fn default() -> Self {
Self::new(T::default())
}
}
impl<T: ArtifactOutput, C: Compiler> ArtifactOutput for Project<C, T> {
type Artifact = T::Artifact;
fn on_output<CP>(
&self,
contracts: &VersionedContracts,
sources: &VersionedSourceFiles,
layout: &ProjectPathsConfig<CP>,
ctx: OutputContext<'_>,
) -> Result<Artifacts<Self::Artifact>> {
self.artifacts_handler().on_output(contracts, sources, layout, ctx)
}
fn handle_artifacts(
&self,
contracts: &VersionedContracts,
artifacts: &Artifacts<Self::Artifact>,
) -> Result<()> {
self.artifacts_handler().handle_artifacts(contracts, artifacts)
}
fn output_file_name(name: &str) -> PathBuf {
T::output_file_name(name)
}
fn output_file_name_versioned(name: &str, version: &Version) -> PathBuf {
T::output_file_name_versioned(name, version)
}
fn output_file(contract_file: &Path, name: &str) -> PathBuf {
T::output_file(contract_file, name)
}
fn output_file_versioned(contract_file: &Path, name: &str, version: &Version) -> PathBuf {
T::output_file_versioned(contract_file, name, version)
}
fn contract_name(file: &Path) -> Option<String> {
T::contract_name(file)
}
fn output_exists(contract_file: &Path, name: &str, root: &Path) -> bool {
T::output_exists(contract_file, name, root)
}
fn read_cached_artifact(path: &Path) -> Result<Self::Artifact> {
T::read_cached_artifact(path)
}
fn read_cached_artifacts<P, I>(files: I) -> Result<BTreeMap<PathBuf, Self::Artifact>>
where
I: IntoIterator<Item = P>,
P: Into<PathBuf>,
{
T::read_cached_artifacts(files)
}
fn contract_to_artifact(
&self,
file: &Path,
name: &str,
contract: Contract,
source_file: Option<&SourceFile>,
) -> Self::Artifact {
self.artifacts_handler().contract_to_artifact(file, name, contract, source_file)
}
fn output_to_artifacts<CP>(
&self,
contracts: &VersionedContracts,
sources: &VersionedSourceFiles,
ctx: OutputContext<'_>,
layout: &ProjectPathsConfig<CP>,
) -> Artifacts<Self::Artifact> {
self.artifacts_handler().output_to_artifacts(contracts, sources, ctx, layout)
}
fn standalone_source_file_to_artifact(
&self,
path: &Path,
file: &VersionedSourceFile,
) -> Option<Self::Artifact> {
self.artifacts_handler().standalone_source_file_to_artifact(path, file)
}
fn is_dirty(&self, artifact_file: &ArtifactFile<Self::Artifact>) -> Result<bool> {
self.artifacts_handler().is_dirty(artifact_file)
}
fn handle_cached_artifacts(&self, artifacts: &Artifacts<Self::Artifact>) -> Result<()> {
self.artifacts_handler().handle_cached_artifacts(artifacts)
}
}
fn rebase_path(base: &Path, path: &Path) -> PathBuf {
use path_slash::PathExt;
let mut base_components = base.components();
let mut path_components = path.components();
let mut new_path = PathBuf::new();
while let Some(path_component) = path_components.next() {
let base_component = base_components.next();
if Some(path_component) != base_component {
if base_component.is_some() {
new_path.extend(
std::iter::repeat(std::path::Component::ParentDir)
.take(base_components.count() + 1),
);
}
new_path.push(path_component);
new_path.extend(path_components);
break;
}
}
new_path.to_slash_lossy().into_owned().into()
}
#[cfg(test)]
#[cfg(feature = "svm-solc")]
mod tests {
use foundry_compilers_artifacts::Remapping;
use foundry_compilers_core::utils::{self, mkdir_or_touch, tempdir};
use super::*;
#[test]
#[cfg_attr(windows, ignore = "<0.7 solc is flaky")]
fn test_build_all_versions() {
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(Default::default())
.unwrap();
let contracts = project.compile().unwrap().succeeded().into_output().contracts;
assert_eq!(contracts.contracts().count(), 3);
}
#[test]
fn test_build_many_libs() {
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"))
.remappings(
Remapping::find_many(&root.join("lib1"))
.into_iter()
.chain(Remapping::find_many(&root.join("lib2"))),
)
.build()
.unwrap();
let project = Project::builder()
.paths(paths)
.no_artifacts()
.ephemeral()
.no_artifacts()
.build(Default::default())
.unwrap();
let contracts = project.compile().unwrap().succeeded().into_output().contracts;
assert_eq!(contracts.contracts().count(), 3);
}
#[test]
fn test_build_remappings() {
let root = utils::canonicalize("../../test-data/test-contract-remappings").unwrap();
let paths = ProjectPathsConfig::builder()
.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(Default::default())
.unwrap();
let contracts = project.compile().unwrap().succeeded().into_output().contracts;
assert_eq!(contracts.contracts().count(), 2);
}
#[test]
fn can_rebase_path() {
let rebase_path = |a: &str, b: &str| rebase_path(a.as_ref(), b.as_ref());
assert_eq!(rebase_path("a/b", "a/b/c"), PathBuf::from("c"));
assert_eq!(rebase_path("a/b", "a/c"), PathBuf::from("../c"));
assert_eq!(rebase_path("a/b", "c"), PathBuf::from("../../c"));
assert_eq!(
rebase_path("/home/user/project", "/home/user/project/A.sol"),
PathBuf::from("A.sol")
);
assert_eq!(
rebase_path("/home/user/project", "/home/user/project/src/A.sol"),
PathBuf::from("src/A.sol")
);
assert_eq!(
rebase_path("/home/user/project", "/home/user/project/lib/forge-std/src/Test.sol"),
PathBuf::from("lib/forge-std/src/Test.sol")
);
assert_eq!(
rebase_path("/home/user/project", "/home/user/A.sol"),
PathBuf::from("../A.sol")
);
assert_eq!(rebase_path("/home/user/project", "/home/A.sol"), PathBuf::from("../../A.sol"));
assert_eq!(rebase_path("/home/user/project", "/A.sol"), PathBuf::from("../../../A.sol"));
assert_eq!(
rebase_path("/home/user/project", "/tmp/A.sol"),
PathBuf::from("../../../tmp/A.sol")
);
assert_eq!(
rebase_path("/Users/ah/temp/verif", "/Users/ah/temp/remapped/Child.sol"),
PathBuf::from("../remapped/Child.sol")
);
assert_eq!(
rebase_path("/Users/ah/temp/verif", "/Users/ah/temp/verif/../remapped/Parent.sol"),
PathBuf::from("../remapped/Parent.sol")
);
}
#[test]
fn can_resolve_oz_remappings() {
let tmp_dir = tempdir("node_modules").unwrap();
let tmp_dir_node_modules = tmp_dir.path().join("node_modules");
let paths = [
"node_modules/@openzeppelin/contracts/interfaces/IERC1155.sol",
"node_modules/@openzeppelin/contracts/finance/VestingWallet.sol",
"node_modules/@openzeppelin/contracts/proxy/Proxy.sol",
"node_modules/@openzeppelin/contracts/token/ERC20/IERC20.sol",
];
mkdir_or_touch(tmp_dir.path(), &paths[..]);
let remappings = Remapping::find_many(&tmp_dir_node_modules);
let mut paths = ProjectPathsConfig::<()>::hardhat(tmp_dir.path()).unwrap();
paths.remappings = remappings;
let resolved = paths
.resolve_library_import(
tmp_dir.path(),
Path::new("@openzeppelin/contracts/token/ERC20/IERC20.sol"),
)
.unwrap();
assert!(resolved.exists());
paths.remappings[0].name = "@openzeppelin/".to_string();
let resolved = paths
.resolve_library_import(
tmp_dir.path(),
Path::new("@openzeppelin/contracts/token/ERC20/IERC20.sol"),
)
.unwrap();
assert!(resolved.exists());
}
}