use crate::{
ArtifactOutput, CompilerSettings, Graph, Project, ProjectCompileOutput, ProjectPathsConfig,
Sources,
artifact_output::Artifacts,
buildinfo::RawBuildInfo,
cache::ArtifactsCache,
compilers::{Compiler, CompilerInput, CompilerOutput, Language},
filter::SparseOutputFilter,
output::{AggregatedCompilerOutput, Builds},
report,
resolver::{GraphEdges, ResolvedSources},
};
use foundry_compilers_core::error::Result;
use rayon::prelude::*;
use semver::Version;
use std::{
collections::{HashMap, HashSet},
fmt::Debug,
path::PathBuf,
time::Instant,
};
pub(crate) type VersionedSources<'a, L, S> = HashMap<L, Vec<(Version, Sources, (&'a str, &'a S))>>;
pub trait Preprocessor<C: Compiler>: Debug {
fn preprocess(
&self,
compiler: &C,
input: &mut C::Input,
paths: &ProjectPathsConfig<C::Language>,
mocks: &mut HashSet<PathBuf>,
) -> Result<()>;
}
#[derive(Debug)]
pub struct ProjectCompiler<
'a,
T: ArtifactOutput<CompilerContract = C::CompilerContract>,
C: Compiler,
> {
edges: GraphEdges<C::Parser>,
project: &'a Project<C, T>,
primary_profiles: HashMap<PathBuf, &'a str>,
sources: CompilerSources<'a, C::Language, C::Settings>,
preprocessor: Option<Box<dyn Preprocessor<C>>>,
}
impl<'a, T: ArtifactOutput<CompilerContract = C::CompilerContract>, C: Compiler>
ProjectCompiler<'a, T, C>
{
pub fn new(project: &'a Project<C, T>) -> Result<Self> {
Self::with_sources(project, project.paths.read_input_files()?)
}
#[instrument(name = "ProjectCompiler::new", skip_all)]
pub fn with_sources(project: &'a Project<C, T>, mut sources: Sources) -> Result<Self> {
if let Some(filter) = &project.sparse_output {
sources.retain(|f, _| filter.is_match(f))
}
let graph = Graph::resolve_sources(&project.paths, sources)?;
let ResolvedSources { sources, primary_profiles, edges } =
graph.into_sources_by_version(project)?;
let jobs_cnt = || sources.values().map(|v| v.len()).sum::<usize>();
let sources = CompilerSources {
jobs: (project.solc_jobs > 1 && jobs_cnt() > 1).then_some(project.solc_jobs),
sources,
};
Ok(Self { edges, primary_profiles, project, sources, preprocessor: None })
}
pub fn with_preprocessor(self, preprocessor: impl Preprocessor<C> + 'static) -> Self {
Self { preprocessor: Some(Box::new(preprocessor)), ..self }
}
#[instrument(name = "compile_project", skip_all)]
pub fn compile(self) -> Result<ProjectCompileOutput<C, T>> {
let slash_paths = self.project.slash_paths;
let mut output = self.preprocess()?.compile()?.write_artifacts()?.write_cache()?;
if slash_paths {
output.slash_paths();
}
Ok(output)
}
#[instrument(skip_all)]
fn preprocess(self) -> Result<PreprocessedState<'a, T, C>> {
trace!("preprocessing");
let Self { edges, project, mut sources, primary_profiles, preprocessor } = self;
sources.slash_paths();
let mut cache = ArtifactsCache::new(project, edges, preprocessor.is_some())?;
sources.filter(&mut cache);
Ok(PreprocessedState { sources, cache, primary_profiles, preprocessor })
}
}
#[derive(Debug)]
struct PreprocessedState<'a, T: ArtifactOutput<CompilerContract = C::CompilerContract>, C: Compiler>
{
sources: CompilerSources<'a, C::Language, C::Settings>,
cache: ArtifactsCache<'a, T, C>,
primary_profiles: HashMap<PathBuf, &'a str>,
preprocessor: Option<Box<dyn Preprocessor<C>>>,
}
impl<'a, T: ArtifactOutput<CompilerContract = C::CompilerContract>, C: Compiler>
PreprocessedState<'a, T, C>
{
#[instrument(skip_all)]
fn compile(self) -> Result<CompiledState<'a, T, C>> {
trace!("compiling");
let PreprocessedState { sources, mut cache, primary_profiles, preprocessor } = self;
let mut output = sources.compile(&mut cache, preprocessor)?;
output.join_all(cache.project().root());
Ok(CompiledState { output, cache, primary_profiles })
}
}
#[derive(Debug)]
struct CompiledState<'a, T: ArtifactOutput<CompilerContract = C::CompilerContract>, C: Compiler> {
output: AggregatedCompilerOutput<C>,
cache: ArtifactsCache<'a, T, C>,
primary_profiles: HashMap<PathBuf, &'a str>,
}
impl<'a, T: ArtifactOutput<CompilerContract = C::CompilerContract>, C: Compiler>
CompiledState<'a, T, C>
{
#[instrument(skip_all)]
fn write_artifacts(self) -> Result<ArtifactsState<'a, T, C>> {
let CompiledState { output, cache, primary_profiles } = self;
let project = cache.project();
let ctx = cache.output_ctx();
let compiled_artifacts = if project.no_artifacts {
project.artifacts_handler().output_to_artifacts(
&output.contracts,
&output.sources,
ctx,
&project.paths,
&primary_profiles,
)
} else if output.has_error(
&project.ignored_error_codes,
&project.ignored_error_codes_from,
&project.ignored_file_paths,
&project.compiler_severity_filter,
) {
trace!("skip writing cache file due to solc errors: {:?}", output.errors);
project.artifacts_handler().output_to_artifacts(
&output.contracts,
&output.sources,
ctx,
&project.paths,
&primary_profiles,
)
} else {
trace!(
"handling artifact output for {} contracts and {} sources",
output.contracts.len(),
output.sources.len()
);
let artifacts = project.artifacts_handler().on_output(
&output.contracts,
&output.sources,
&project.paths,
ctx,
&primary_profiles,
)?;
output.write_build_infos(project.build_info_path())?;
artifacts
};
Ok(ArtifactsState { output, cache, compiled_artifacts })
}
}
#[derive(Debug)]
struct ArtifactsState<'a, T: ArtifactOutput<CompilerContract = C::CompilerContract>, C: Compiler> {
output: AggregatedCompilerOutput<C>,
cache: ArtifactsCache<'a, T, C>,
compiled_artifacts: Artifacts<T::Artifact>,
}
impl<T: ArtifactOutput<CompilerContract = C::CompilerContract>, C: Compiler>
ArtifactsState<'_, T, C>
{
#[instrument(skip_all)]
fn write_cache(self) -> Result<ProjectCompileOutput<C, T>> {
let ArtifactsState { output, cache, compiled_artifacts } = self;
let project = cache.project();
let ignored_error_codes = project.ignored_error_codes.clone();
let ignored_error_codes_from = project.ignored_error_codes_from.clone();
let ignored_file_paths = project.ignored_file_paths.clone();
let compiler_severity_filter = project.compiler_severity_filter;
let has_error = output.has_error(
&ignored_error_codes,
&ignored_error_codes_from,
&ignored_file_paths,
&compiler_severity_filter,
);
let skip_write_to_disk = project.no_artifacts || has_error;
trace!(has_error, project.no_artifacts, skip_write_to_disk, cache_path=?project.cache_path(),"prepare writing cache file");
let (cached_artifacts, cached_builds, edges) =
cache.consume(&compiled_artifacts, &output.build_infos, !skip_write_to_disk)?;
project.artifacts_handler().handle_cached_artifacts(&cached_artifacts)?;
let builds = Builds(
output
.build_infos
.iter()
.map(|build_info| (build_info.id.clone(), build_info.build_context.clone()))
.chain(cached_builds)
.map(|(id, context)| (id, context.with_joined_paths(project.paths.root.as_path())))
.collect(),
);
Ok(ProjectCompileOutput {
compiler_output: output,
compiled_artifacts,
cached_artifacts,
ignored_error_codes,
ignored_error_codes_from,
ignored_file_paths,
compiler_severity_filter,
builds,
edges,
})
}
}
#[derive(Debug, Clone)]
struct CompilerSources<'a, L, S> {
sources: VersionedSources<'a, L, S>,
jobs: Option<usize>,
}
impl<L: Language, S: CompilerSettings> CompilerSources<'_, L, S> {
#[allow(clippy::missing_const_for_fn)]
fn slash_paths(&mut self) {
#[cfg(windows)]
{
use path_slash::PathBufExt;
self.sources.values_mut().for_each(|versioned_sources| {
versioned_sources.iter_mut().for_each(|(_, sources, _)| {
*sources = std::mem::take(sources)
.into_iter()
.map(|(path, source)| {
(PathBuf::from(path.to_slash_lossy().as_ref()), source)
})
.collect()
})
});
}
}
#[instrument(name = "CompilerSources::filter", skip_all)]
fn filter<
T: ArtifactOutput<CompilerContract = C::CompilerContract>,
C: Compiler<Language = L>,
>(
&mut self,
cache: &mut ArtifactsCache<'_, T, C>,
) {
cache.remove_dirty_sources();
for versioned_sources in self.sources.values_mut() {
for (version, sources, (profile, _)) in versioned_sources {
trace!("Filtering {} sources for {}", sources.len(), version);
cache.filter(sources, version, profile);
trace!(
"Detected {} sources to compile {:?}",
sources.dirty().count(),
sources.dirty_files().collect::<Vec<_>>()
);
}
}
}
fn compile<
C: Compiler<Language = L, Settings = S>,
T: ArtifactOutput<CompilerContract = C::CompilerContract>,
>(
self,
cache: &mut ArtifactsCache<'_, T, C>,
preprocessor: Option<Box<dyn Preprocessor<C>>>,
) -> Result<AggregatedCompilerOutput<C>> {
let project = cache.project();
let graph = cache.graph();
let jobs_cnt = self.jobs;
let sparse_output = SparseOutputFilter::new(project.sparse_output.as_deref());
let mut include_paths = project.paths.include_paths.clone();
include_paths.extend(graph.include_paths().clone());
let mut mocks = cache.mocks();
let mut jobs = Vec::new();
for (language, versioned_sources) in self.sources {
for (version, sources, (profile, opt_settings)) in versioned_sources {
let mut opt_settings = opt_settings.clone();
if sources.is_empty() {
trace!("skip {} for empty sources set", version);
continue;
}
let actually_dirty =
sparse_output.sparse_sources(&sources, &mut opt_settings, graph);
if actually_dirty.is_empty() {
trace!("skip {} run due to empty source set", version);
continue;
}
trace!("calling {} with {} sources {:?}", version, sources.len(), sources.keys());
let settings = opt_settings
.with_base_path(&project.paths.root)
.with_allow_paths(&project.paths.allowed_paths)
.with_include_paths(&include_paths)
.with_remappings(&project.paths.remappings);
let mut input = C::Input::build(sources, settings, language, version.clone());
input.strip_prefix(project.paths.root.as_path());
if let Some(preprocessor) = preprocessor.as_ref() {
preprocessor.preprocess(
&project.compiler,
&mut input,
&project.paths,
&mut mocks,
)?;
}
jobs.push((input, profile, actually_dirty));
}
}
cache.update_mocks(mocks);
let results = if let Some(num_jobs) = jobs_cnt {
compile_parallel(&project.compiler, jobs, num_jobs)
} else {
compile_sequential(&project.compiler, jobs)
}?;
let mut aggregated = AggregatedCompilerOutput::default();
for (input, mut output, profile, actually_dirty) in results {
let version = input.version();
for file in &actually_dirty {
cache.compiler_seen(file);
}
let build_info = RawBuildInfo::new(&input, &output, project.build_info)?;
output.retain_files(
actually_dirty
.iter()
.map(|f| f.strip_prefix(project.paths.root.as_path()).unwrap_or(f)),
);
output.join_all(project.paths.root.as_path());
aggregated.extend(version.clone(), build_info, profile, output);
}
Ok(aggregated)
}
}
type CompilationResult<'a, I, E, C> = Result<Vec<(I, CompilerOutput<E, C>, &'a str, Vec<PathBuf>)>>;
fn compile_sequential<'a, C: Compiler>(
compiler: &C,
jobs: Vec<(C::Input, &'a str, Vec<PathBuf>)>,
) -> CompilationResult<'a, C::Input, C::CompilationError, C::CompilerContract> {
jobs.into_iter()
.map(|(input, profile, actually_dirty)| {
let start = Instant::now();
report::compiler_spawn(
&input.compiler_name(),
input.version(),
actually_dirty.as_slice(),
);
let output = compiler.compile(&input)?;
report::compiler_success(&input.compiler_name(), input.version(), &start.elapsed());
Ok((input, output, profile, actually_dirty))
})
.collect()
}
fn compile_parallel<'a, C: Compiler>(
compiler: &C,
jobs: Vec<(C::Input, &'a str, Vec<PathBuf>)>,
num_jobs: usize,
) -> CompilationResult<'a, C::Input, C::CompilationError, C::CompilerContract> {
let scoped_report = report::get_default(|reporter| reporter.clone());
let pool = rayon::ThreadPoolBuilder::new().num_threads(num_jobs).build().unwrap();
pool.install(move || {
jobs.into_par_iter()
.map(move |(input, profile, actually_dirty)| {
let _guard = report::set_scoped(&scoped_report);
let start = Instant::now();
report::compiler_spawn(
&input.compiler_name(),
input.version(),
actually_dirty.as_slice(),
);
compiler.compile(&input).map(move |output| {
report::compiler_success(
&input.compiler_name(),
input.version(),
&start.elapsed(),
);
(input, output, profile, actually_dirty)
})
})
.collect()
})
}
#[cfg(test)]
#[cfg(all(feature = "project-util", feature = "svm-solc"))]
mod tests {
use std::path::Path;
use foundry_compilers_artifacts::output_selection::ContractOutputSelection;
use crate::{
ConfigurableArtifacts, MinimalCombinedArtifacts, compilers::multi::MultiCompiler,
project_util::TempProject,
};
use super::*;
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 = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../test-data/dapp-sample");
let project = Project::builder()
.paths(ProjectPathsConfig::dapptools(&root).unwrap())
.build(Default::default())
.unwrap();
let compiler = ProjectCompiler::new(&project).unwrap();
let prep = compiler.preprocess().unwrap();
let cache = prep.cache.as_cached().unwrap();
assert_eq!(cache.cache.files.len(), 3);
assert!(cache.cache.files.values().all(|v| v.artifacts.is_empty()));
let compiled = prep.compile().unwrap();
assert_eq!(compiled.output.contracts.files().count(), 3);
}
#[test]
fn can_detect_cached_files() {
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../test-data/dapp-sample");
let paths = ProjectPathsConfig::builder().sources(root.join("src")).lib(root.join("lib"));
let project = TempProject::<MultiCompiler, MinimalCombinedArtifacts>::new(paths).unwrap();
let compiled = project.compile().unwrap();
compiled.assert_success();
let inner = project.project();
let compiler = ProjectCompiler::new(inner).unwrap();
let prep = compiler.preprocess().unwrap();
assert!(prep.cache.as_cached().unwrap().dirty_sources.is_empty())
}
#[test]
fn can_recompile_with_optimized_output() {
let tmp = TempProject::<MultiCompiler, ConfigurableArtifacts>::dapptools().unwrap();
tmp.add_source(
"A",
r#"
pragma solidity ^0.8.10;
import "./B.sol";
contract A {}
"#,
)
.unwrap();
tmp.add_source(
"B",
r#"
pragma solidity ^0.8.10;
contract B {
function hello() public {}
}
import "./C.sol";
"#,
)
.unwrap();
tmp.add_source(
"C",
r"
pragma solidity ^0.8.10;
contract C {
function hello() public {}
}
",
)
.unwrap();
let compiled = tmp.compile().unwrap();
compiled.assert_success();
tmp.artifacts_snapshot().unwrap().assert_artifacts_essentials_present();
tmp.add_source(
"A",
r#"
pragma solidity ^0.8.10;
import "./B.sol";
contract A {
function testExample() public {}
}
"#,
)
.unwrap();
let compiler = ProjectCompiler::new(tmp.project()).unwrap();
let state = compiler.preprocess().unwrap();
let sources = &state.sources.sources;
let cache = state.cache.as_cached().unwrap();
assert_eq!(cache.cache.artifacts_len(), 2);
assert!(cache.cache.all_artifacts_exist());
assert_eq!(cache.dirty_sources.len(), 1);
let len = sources.values().map(|v| v.len()).sum::<usize>();
assert_eq!(len, 1);
let filtered = &sources.values().next().unwrap()[0].1;
assert_eq!(filtered.0.len(), 3);
assert_eq!(filtered.dirty().count(), 1);
assert!(filtered.dirty_files().next().unwrap().ends_with("A.sol"));
let state = state.compile().unwrap();
assert_eq!(state.output.sources.len(), 1);
for (f, source) in state.output.sources.sources() {
if f.ends_with("A.sol") {
assert!(source.ast.is_some());
} else {
assert!(source.ast.is_none());
}
}
assert_eq!(state.output.contracts.len(), 1);
let (a, c) = state.output.contracts_iter().next().unwrap();
assert_eq!(a, "A");
assert!(c.abi.is_some() && c.evm.is_some());
let state = state.write_artifacts().unwrap();
assert_eq!(state.compiled_artifacts.as_ref().len(), 1);
let out = state.write_cache().unwrap();
let artifacts: Vec<_> = out.into_artifacts().collect();
assert_eq!(artifacts.len(), 3);
for (_, artifact) in artifacts {
let c = artifact.into_contract_bytecode();
assert!(c.abi.is_some() && c.bytecode.is_some() && c.deployed_bytecode.is_some());
}
tmp.artifacts_snapshot().unwrap().assert_artifacts_essentials_present();
}
#[test]
#[ignore]
fn can_compile_real_project() {
init_tracing();
let paths = ProjectPathsConfig::builder()
.root("../../foundry-integration-tests/testdata/solmate")
.build()
.unwrap();
let project = Project::builder().paths(paths).build(Default::default()).unwrap();
let compiler = ProjectCompiler::new(&project).unwrap();
let _out = compiler.compile().unwrap();
}
#[test]
fn extra_output_cached() {
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../test-data/dapp-sample");
let paths = ProjectPathsConfig::builder().sources(root.join("src")).lib(root.join("lib"));
let mut project = TempProject::<MultiCompiler>::new(paths).unwrap();
project.compile().unwrap();
project.project_mut().artifacts =
ConfigurableArtifacts::new([], [ContractOutputSelection::Abi]);
let abi_path = project.project().paths.artifacts.join("Dapp.sol/Dapp.abi.json");
assert!(!abi_path.exists());
let output = project.compile().unwrap();
assert!(output.compiler_output.is_empty());
assert!(abi_path.exists());
}
#[test]
fn can_compile_leftovers_after_sparse() {
let mut tmp = TempProject::<MultiCompiler, ConfigurableArtifacts>::dapptools().unwrap();
tmp.add_source(
"A",
r#"
pragma solidity ^0.8.10;
import "./B.sol";
contract A {}
"#,
)
.unwrap();
tmp.add_source(
"B",
r#"
pragma solidity ^0.8.10;
contract B {}
"#,
)
.unwrap();
tmp.project_mut().sparse_output = Some(Box::new(|f: &Path| f.ends_with("A.sol")));
let compiled = tmp.compile().unwrap();
compiled.assert_success();
assert_eq!(compiled.artifacts().count(), 1);
tmp.project_mut().sparse_output = None;
let compiled = tmp.compile().unwrap();
compiled.assert_success();
assert_eq!(compiled.artifacts().count(), 2);
}
}