1use crate::{
104 artifact_output::Artifacts,
105 buildinfo::RawBuildInfo,
106 cache::ArtifactsCache,
107 compilers::{Compiler, CompilerInput, CompilerOutput, Language},
108 filter::SparseOutputFilter,
109 output::{AggregatedCompilerOutput, Builds},
110 report,
111 resolver::{GraphEdges, ResolvedSources},
112 ArtifactOutput, CompilerSettings, Graph, Project, ProjectCompileOutput, ProjectPathsConfig,
113 Sources,
114};
115use foundry_compilers_core::error::Result;
116use rayon::prelude::*;
117use semver::Version;
118use std::{
119 collections::{HashMap, HashSet},
120 fmt::Debug,
121 path::PathBuf,
122 time::Instant,
123};
124
125pub(crate) type VersionedSources<'a, L, S> = HashMap<L, Vec<(Version, Sources, (&'a str, &'a S))>>;
127
128pub trait Preprocessor<C: Compiler>: Debug {
133 fn preprocess(
134 &self,
135 compiler: &C,
136 input: &mut C::Input,
137 paths: &ProjectPathsConfig<C::Language>,
138 mocks: &mut HashSet<PathBuf>,
139 ) -> Result<()>;
140}
141
142#[derive(Debug)]
143pub struct ProjectCompiler<
144 'a,
145 T: ArtifactOutput<CompilerContract = C::CompilerContract>,
146 C: Compiler,
147> {
148 edges: GraphEdges<C::Parser>,
150 project: &'a Project<C, T>,
151 primary_profiles: HashMap<PathBuf, &'a str>,
153 sources: CompilerSources<'a, C::Language, C::Settings>,
155 preprocessor: Option<Box<dyn Preprocessor<C>>>,
157}
158
159impl<'a, T: ArtifactOutput<CompilerContract = C::CompilerContract>, C: Compiler>
160 ProjectCompiler<'a, T, C>
161{
162 pub fn new(project: &'a Project<C, T>) -> Result<Self> {
165 Self::with_sources(project, project.paths.read_input_files()?)
166 }
167
168 #[instrument(name = "ProjectCompiler::new", skip_all)]
175 pub fn with_sources(project: &'a Project<C, T>, mut sources: Sources) -> Result<Self> {
176 if let Some(filter) = &project.sparse_output {
177 sources.retain(|f, _| filter.is_match(f))
178 }
179 let graph = Graph::resolve_sources(&project.paths, sources)?;
180 let ResolvedSources { sources, primary_profiles, edges } =
181 graph.into_sources_by_version(project)?;
182
183 let jobs_cnt = || sources.values().map(|v| v.len()).sum::<usize>();
186 let sources = CompilerSources {
187 jobs: (project.solc_jobs > 1 && jobs_cnt() > 1).then_some(project.solc_jobs),
188 sources,
189 };
190
191 Ok(Self { edges, primary_profiles, project, sources, preprocessor: None })
192 }
193
194 pub fn with_preprocessor(self, preprocessor: impl Preprocessor<C> + 'static) -> Self {
195 Self { preprocessor: Some(Box::new(preprocessor)), ..self }
196 }
197
198 #[instrument(name = "compile_project", skip_all)]
214 pub fn compile(self) -> Result<ProjectCompileOutput<C, T>> {
215 let slash_paths = self.project.slash_paths;
216
217 let mut output = self.preprocess()?.compile()?.write_artifacts()?.write_cache()?;
219
220 if slash_paths {
221 output.slash_paths();
223 }
224
225 Ok(output)
226 }
227
228 #[instrument(skip_all)]
232 fn preprocess(self) -> Result<PreprocessedState<'a, T, C>> {
233 trace!("preprocessing");
234 let Self { edges, project, mut sources, primary_profiles, preprocessor } = self;
235
236 sources.slash_paths();
239
240 let mut cache = ArtifactsCache::new(project, edges, preprocessor.is_some())?;
241 sources.filter(&mut cache);
243
244 Ok(PreprocessedState { sources, cache, primary_profiles, preprocessor })
245 }
246}
247
248#[derive(Debug)]
252struct PreprocessedState<'a, T: ArtifactOutput<CompilerContract = C::CompilerContract>, C: Compiler>
253{
254 sources: CompilerSources<'a, C::Language, C::Settings>,
256
257 cache: ArtifactsCache<'a, T, C>,
259
260 primary_profiles: HashMap<PathBuf, &'a str>,
262
263 preprocessor: Option<Box<dyn Preprocessor<C>>>,
265}
266
267impl<'a, T: ArtifactOutput<CompilerContract = C::CompilerContract>, C: Compiler>
268 PreprocessedState<'a, T, C>
269{
270 #[instrument(skip_all)]
272 fn compile(self) -> Result<CompiledState<'a, T, C>> {
273 trace!("compiling");
274 let PreprocessedState { sources, mut cache, primary_profiles, preprocessor } = self;
275
276 let mut output = sources.compile(&mut cache, preprocessor)?;
277
278 output.join_all(cache.project().root());
284
285 Ok(CompiledState { output, cache, primary_profiles })
286 }
287}
288
289#[derive(Debug)]
291struct CompiledState<'a, T: ArtifactOutput<CompilerContract = C::CompilerContract>, C: Compiler> {
292 output: AggregatedCompilerOutput<C>,
293 cache: ArtifactsCache<'a, T, C>,
294 primary_profiles: HashMap<PathBuf, &'a str>,
295}
296
297impl<'a, T: ArtifactOutput<CompilerContract = C::CompilerContract>, C: Compiler>
298 CompiledState<'a, T, C>
299{
300 #[instrument(skip_all)]
305 fn write_artifacts(self) -> Result<ArtifactsState<'a, T, C>> {
306 let CompiledState { output, cache, primary_profiles } = self;
307
308 let project = cache.project();
309 let ctx = cache.output_ctx();
310 let compiled_artifacts = if project.no_artifacts {
313 project.artifacts_handler().output_to_artifacts(
314 &output.contracts,
315 &output.sources,
316 ctx,
317 &project.paths,
318 &primary_profiles,
319 )
320 } else if output.has_error(
321 &project.ignored_error_codes,
322 &project.ignored_file_paths,
323 &project.compiler_severity_filter,
324 ) {
325 trace!("skip writing cache file due to solc errors: {:?}", output.errors);
326 project.artifacts_handler().output_to_artifacts(
327 &output.contracts,
328 &output.sources,
329 ctx,
330 &project.paths,
331 &primary_profiles,
332 )
333 } else {
334 trace!(
335 "handling artifact output for {} contracts and {} sources",
336 output.contracts.len(),
337 output.sources.len()
338 );
339 let artifacts = project.artifacts_handler().on_output(
341 &output.contracts,
342 &output.sources,
343 &project.paths,
344 ctx,
345 &primary_profiles,
346 )?;
347
348 output.write_build_infos(project.build_info_path())?;
350
351 artifacts
352 };
353
354 Ok(ArtifactsState { output, cache, compiled_artifacts })
355 }
356}
357
358#[derive(Debug)]
360struct ArtifactsState<'a, T: ArtifactOutput<CompilerContract = C::CompilerContract>, C: Compiler> {
361 output: AggregatedCompilerOutput<C>,
362 cache: ArtifactsCache<'a, T, C>,
363 compiled_artifacts: Artifacts<T::Artifact>,
364}
365
366impl<T: ArtifactOutput<CompilerContract = C::CompilerContract>, C: Compiler>
367 ArtifactsState<'_, T, C>
368{
369 #[instrument(skip_all)]
373 fn write_cache(self) -> Result<ProjectCompileOutput<C, T>> {
374 let ArtifactsState { output, cache, compiled_artifacts } = self;
375 let project = cache.project();
376 let ignored_error_codes = project.ignored_error_codes.clone();
377 let ignored_file_paths = project.ignored_file_paths.clone();
378 let compiler_severity_filter = project.compiler_severity_filter;
379 let has_error =
380 output.has_error(&ignored_error_codes, &ignored_file_paths, &compiler_severity_filter);
381 let skip_write_to_disk = project.no_artifacts || has_error;
382 trace!(has_error, project.no_artifacts, skip_write_to_disk, cache_path=?project.cache_path(),"prepare writing cache file");
383
384 let (cached_artifacts, cached_builds, edges) =
385 cache.consume(&compiled_artifacts, &output.build_infos, !skip_write_to_disk)?;
386
387 project.artifacts_handler().handle_cached_artifacts(&cached_artifacts)?;
388
389 let builds = Builds(
390 output
391 .build_infos
392 .iter()
393 .map(|build_info| (build_info.id.clone(), build_info.build_context.clone()))
394 .chain(cached_builds)
395 .map(|(id, context)| (id, context.with_joined_paths(project.paths.root.as_path())))
396 .collect(),
397 );
398
399 Ok(ProjectCompileOutput {
400 compiler_output: output,
401 compiled_artifacts,
402 cached_artifacts,
403 ignored_error_codes,
404 ignored_file_paths,
405 compiler_severity_filter,
406 builds,
407 edges,
408 })
409 }
410}
411
412#[derive(Debug, Clone)]
414struct CompilerSources<'a, L, S> {
415 sources: VersionedSources<'a, L, S>,
417 jobs: Option<usize>,
419}
420
421impl<L: Language, S: CompilerSettings> CompilerSources<'_, L, S> {
422 fn slash_paths(&mut self) {
427 #[cfg(windows)]
428 {
429 use path_slash::PathBufExt;
430
431 self.sources.values_mut().for_each(|versioned_sources| {
432 versioned_sources.iter_mut().for_each(|(_, sources, _)| {
433 *sources = std::mem::take(sources)
434 .into_iter()
435 .map(|(path, source)| {
436 (PathBuf::from(path.to_slash_lossy().as_ref()), source)
437 })
438 .collect()
439 })
440 });
441 }
442 }
443
444 #[instrument(name = "CompilerSources::filter", skip_all)]
446 fn filter<
447 T: ArtifactOutput<CompilerContract = C::CompilerContract>,
448 C: Compiler<Language = L>,
449 >(
450 &mut self,
451 cache: &mut ArtifactsCache<'_, T, C>,
452 ) {
453 cache.remove_dirty_sources();
454 for versioned_sources in self.sources.values_mut() {
455 for (version, sources, (profile, _)) in versioned_sources {
456 trace!("Filtering {} sources for {}", sources.len(), version);
457 cache.filter(sources, version, profile);
458 trace!(
459 "Detected {} sources to compile {:?}",
460 sources.dirty().count(),
461 sources.dirty_files().collect::<Vec<_>>()
462 );
463 }
464 }
465 }
466
467 fn compile<
469 C: Compiler<Language = L, Settings = S>,
470 T: ArtifactOutput<CompilerContract = C::CompilerContract>,
471 >(
472 self,
473 cache: &mut ArtifactsCache<'_, T, C>,
474 preprocessor: Option<Box<dyn Preprocessor<C>>>,
475 ) -> Result<AggregatedCompilerOutput<C>> {
476 let project = cache.project();
477 let graph = cache.graph();
478
479 let jobs_cnt = self.jobs;
480
481 let sparse_output = SparseOutputFilter::new(project.sparse_output.as_deref());
482
483 let mut include_paths = project.paths.include_paths.clone();
485 include_paths.extend(graph.include_paths().clone());
486
487 let mut mocks = cache.mocks();
490
491 let mut jobs = Vec::new();
492 for (language, versioned_sources) in self.sources {
493 for (version, sources, (profile, opt_settings)) in versioned_sources {
494 let mut opt_settings = opt_settings.clone();
495 if sources.is_empty() {
496 trace!("skip {} for empty sources set", version);
498 continue;
499 }
500
501 let actually_dirty =
504 sparse_output.sparse_sources(&sources, &mut opt_settings, graph);
505
506 if actually_dirty.is_empty() {
507 trace!("skip {} run due to empty source set", version);
510 continue;
511 }
512
513 trace!("calling {} with {} sources {:?}", version, sources.len(), sources.keys());
514
515 let settings = opt_settings
516 .with_base_path(&project.paths.root)
517 .with_allow_paths(&project.paths.allowed_paths)
518 .with_include_paths(&include_paths)
519 .with_remappings(&project.paths.remappings);
520
521 let mut input = C::Input::build(sources, settings, language, version.clone());
522
523 input.strip_prefix(project.paths.root.as_path());
524
525 if let Some(preprocessor) = preprocessor.as_ref() {
526 preprocessor.preprocess(
527 &project.compiler,
528 &mut input,
529 &project.paths,
530 &mut mocks,
531 )?;
532 }
533
534 jobs.push((input, profile, actually_dirty));
535 }
536 }
537
538 cache.update_mocks(mocks);
540
541 let results = if let Some(num_jobs) = jobs_cnt {
542 compile_parallel(&project.compiler, jobs, num_jobs)
543 } else {
544 compile_sequential(&project.compiler, jobs)
545 }?;
546
547 let mut aggregated = AggregatedCompilerOutput::default();
548
549 for (input, mut output, profile, actually_dirty) in results {
550 let version = input.version();
551
552 for file in &actually_dirty {
554 cache.compiler_seen(file);
555 }
556
557 let build_info = RawBuildInfo::new(&input, &output, project.build_info)?;
558
559 output.retain_files(
560 actually_dirty
561 .iter()
562 .map(|f| f.strip_prefix(project.paths.root.as_path()).unwrap_or(f)),
563 );
564 output.join_all(project.paths.root.as_path());
565
566 aggregated.extend(version.clone(), build_info, profile, output);
567 }
568
569 Ok(aggregated)
570 }
571}
572
573type CompilationResult<'a, I, E, C> = Result<Vec<(I, CompilerOutput<E, C>, &'a str, Vec<PathBuf>)>>;
574
575fn compile_sequential<'a, C: Compiler>(
577 compiler: &C,
578 jobs: Vec<(C::Input, &'a str, Vec<PathBuf>)>,
579) -> CompilationResult<'a, C::Input, C::CompilationError, C::CompilerContract> {
580 jobs.into_iter()
581 .map(|(input, profile, actually_dirty)| {
582 let start = Instant::now();
583 report::compiler_spawn(
584 &input.compiler_name(),
585 input.version(),
586 actually_dirty.as_slice(),
587 );
588 let output = compiler.compile(&input)?;
589 report::compiler_success(&input.compiler_name(), input.version(), &start.elapsed());
590
591 Ok((input, output, profile, actually_dirty))
592 })
593 .collect()
594}
595
596fn compile_parallel<'a, C: Compiler>(
598 compiler: &C,
599 jobs: Vec<(C::Input, &'a str, Vec<PathBuf>)>,
600 num_jobs: usize,
601) -> CompilationResult<'a, C::Input, C::CompilationError, C::CompilerContract> {
602 let scoped_report = report::get_default(|reporter| reporter.clone());
606
607 let pool = rayon::ThreadPoolBuilder::new().num_threads(num_jobs).build().unwrap();
609
610 pool.install(move || {
611 jobs.into_par_iter()
612 .map(move |(input, profile, actually_dirty)| {
613 let _guard = report::set_scoped(&scoped_report);
615
616 let start = Instant::now();
617 report::compiler_spawn(
618 &input.compiler_name(),
619 input.version(),
620 actually_dirty.as_slice(),
621 );
622 compiler.compile(&input).map(move |output| {
623 report::compiler_success(
624 &input.compiler_name(),
625 input.version(),
626 &start.elapsed(),
627 );
628 (input, output, profile, actually_dirty)
629 })
630 })
631 .collect()
632 })
633}
634
635#[cfg(test)]
636#[cfg(all(feature = "project-util", feature = "svm-solc"))]
637mod tests {
638 use std::path::Path;
639
640 use foundry_compilers_artifacts::output_selection::ContractOutputSelection;
641
642 use crate::{
643 compilers::multi::MultiCompiler, project_util::TempProject, ConfigurableArtifacts,
644 MinimalCombinedArtifacts, ProjectPathsConfig,
645 };
646
647 use super::*;
648
649 fn init_tracing() {
650 let _ = tracing_subscriber::fmt()
651 .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
652 .try_init()
653 .ok();
654 }
655
656 #[test]
657 fn can_preprocess() {
658 let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../test-data/dapp-sample");
659 let project = Project::builder()
660 .paths(ProjectPathsConfig::dapptools(&root).unwrap())
661 .build(Default::default())
662 .unwrap();
663
664 let compiler = ProjectCompiler::new(&project).unwrap();
665 let prep = compiler.preprocess().unwrap();
666 let cache = prep.cache.as_cached().unwrap();
667 assert_eq!(cache.cache.files.len(), 3);
669 assert!(cache.cache.files.values().all(|v| v.artifacts.is_empty()));
670
671 let compiled = prep.compile().unwrap();
672 assert_eq!(compiled.output.contracts.files().count(), 3);
673 }
674
675 #[test]
676 fn can_detect_cached_files() {
677 let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../test-data/dapp-sample");
678 let paths = ProjectPathsConfig::builder().sources(root.join("src")).lib(root.join("lib"));
679 let project = TempProject::<MultiCompiler, MinimalCombinedArtifacts>::new(paths).unwrap();
680
681 let compiled = project.compile().unwrap();
682 compiled.assert_success();
683
684 let inner = project.project();
685 let compiler = ProjectCompiler::new(inner).unwrap();
686 let prep = compiler.preprocess().unwrap();
687 assert!(prep.cache.as_cached().unwrap().dirty_sources.is_empty())
688 }
689
690 #[test]
691 fn can_recompile_with_optimized_output() {
692 let tmp = TempProject::<MultiCompiler, ConfigurableArtifacts>::dapptools().unwrap();
693
694 tmp.add_source(
695 "A",
696 r#"
697 pragma solidity ^0.8.10;
698 import "./B.sol";
699 contract A {}
700 "#,
701 )
702 .unwrap();
703
704 tmp.add_source(
705 "B",
706 r#"
707 pragma solidity ^0.8.10;
708 contract B {
709 function hello() public {}
710 }
711 import "./C.sol";
712 "#,
713 )
714 .unwrap();
715
716 tmp.add_source(
717 "C",
718 r"
719 pragma solidity ^0.8.10;
720 contract C {
721 function hello() public {}
722 }
723 ",
724 )
725 .unwrap();
726 let compiled = tmp.compile().unwrap();
727 compiled.assert_success();
728
729 tmp.artifacts_snapshot().unwrap().assert_artifacts_essentials_present();
730
731 tmp.add_source(
733 "A",
734 r#"
735 pragma solidity ^0.8.10;
736 import "./B.sol";
737 contract A {
738 function testExample() public {}
739 }
740 "#,
741 )
742 .unwrap();
743
744 let compiler = ProjectCompiler::new(tmp.project()).unwrap();
745 let state = compiler.preprocess().unwrap();
746 let sources = &state.sources.sources;
747
748 let cache = state.cache.as_cached().unwrap();
749
750 assert_eq!(cache.cache.artifacts_len(), 2);
752 assert!(cache.cache.all_artifacts_exist());
753 assert_eq!(cache.dirty_sources.len(), 1);
754
755 let len = sources.values().map(|v| v.len()).sum::<usize>();
756 assert_eq!(len, 1);
758
759 let filtered = &sources.values().next().unwrap()[0].1;
760
761 assert_eq!(filtered.0.len(), 3);
763 assert_eq!(filtered.dirty().count(), 1);
765 assert!(filtered.dirty_files().next().unwrap().ends_with("A.sol"));
766
767 let state = state.compile().unwrap();
768 assert_eq!(state.output.sources.len(), 1);
769 for (f, source) in state.output.sources.sources() {
770 if f.ends_with("A.sol") {
771 assert!(source.ast.is_some());
772 } else {
773 assert!(source.ast.is_none());
774 }
775 }
776
777 assert_eq!(state.output.contracts.len(), 1);
778 let (a, c) = state.output.contracts_iter().next().unwrap();
779 assert_eq!(a, "A");
780 assert!(c.abi.is_some() && c.evm.is_some());
781
782 let state = state.write_artifacts().unwrap();
783 assert_eq!(state.compiled_artifacts.as_ref().len(), 1);
784
785 let out = state.write_cache().unwrap();
786
787 let artifacts: Vec<_> = out.into_artifacts().collect();
788 assert_eq!(artifacts.len(), 3);
789 for (_, artifact) in artifacts {
790 let c = artifact.into_contract_bytecode();
791 assert!(c.abi.is_some() && c.bytecode.is_some() && c.deployed_bytecode.is_some());
792 }
793
794 tmp.artifacts_snapshot().unwrap().assert_artifacts_essentials_present();
795 }
796
797 #[test]
798 #[ignore]
799 fn can_compile_real_project() {
800 init_tracing();
801 let paths = ProjectPathsConfig::builder()
802 .root("../../foundry-integration-tests/testdata/solmate")
803 .build()
804 .unwrap();
805 let project = Project::builder().paths(paths).build(Default::default()).unwrap();
806 let compiler = ProjectCompiler::new(&project).unwrap();
807 let _out = compiler.compile().unwrap();
808 }
809
810 #[test]
811 fn extra_output_cached() {
812 let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../test-data/dapp-sample");
813 let paths = ProjectPathsConfig::builder().sources(root.join("src")).lib(root.join("lib"));
814 let mut project = TempProject::<MultiCompiler>::new(paths).unwrap();
815
816 project.compile().unwrap();
818
819 project.project_mut().artifacts =
821 ConfigurableArtifacts::new([], [ContractOutputSelection::Abi]);
822
823 let abi_path = project.project().paths.artifacts.join("Dapp.sol/Dapp.abi.json");
825 assert!(!abi_path.exists());
826 let output = project.compile().unwrap();
827 assert!(output.compiler_output.is_empty());
828 assert!(abi_path.exists());
829 }
830
831 #[test]
832 fn can_compile_leftovers_after_sparse() {
833 let mut tmp = TempProject::<MultiCompiler, ConfigurableArtifacts>::dapptools().unwrap();
834
835 tmp.add_source(
836 "A",
837 r#"
838pragma solidity ^0.8.10;
839import "./B.sol";
840contract A {}
841"#,
842 )
843 .unwrap();
844
845 tmp.add_source(
846 "B",
847 r#"
848pragma solidity ^0.8.10;
849contract B {}
850"#,
851 )
852 .unwrap();
853
854 tmp.project_mut().sparse_output = Some(Box::new(|f: &Path| f.ends_with("A.sol")));
855 let compiled = tmp.compile().unwrap();
856 compiled.assert_success();
857 assert_eq!(compiled.artifacts().count(), 1);
858
859 tmp.project_mut().sparse_output = None;
860 let compiled = tmp.compile().unwrap();
861 compiled.assert_success();
862 assert_eq!(compiled.artifacts().count(), 2);
863 }
864}