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::ParsedSource>,
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) =
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 })
408 }
409}
410
411#[derive(Debug, Clone)]
413struct CompilerSources<'a, L, S> {
414 sources: VersionedSources<'a, L, S>,
416 jobs: Option<usize>,
418}
419
420impl<L: Language, S: CompilerSettings> CompilerSources<'_, L, S> {
421 fn slash_paths(&mut self) {
426 #[cfg(windows)]
427 {
428 use path_slash::PathBufExt;
429
430 self.sources.values_mut().for_each(|versioned_sources| {
431 versioned_sources.iter_mut().for_each(|(_, sources, _)| {
432 *sources = std::mem::take(sources)
433 .into_iter()
434 .map(|(path, source)| {
435 (PathBuf::from(path.to_slash_lossy().as_ref()), source)
436 })
437 .collect()
438 })
439 });
440 }
441 }
442
443 #[instrument(name = "CompilerSources::filter", skip_all)]
445 fn filter<
446 T: ArtifactOutput<CompilerContract = C::CompilerContract>,
447 C: Compiler<Language = L>,
448 >(
449 &mut self,
450 cache: &mut ArtifactsCache<'_, T, C>,
451 ) {
452 cache.remove_dirty_sources();
453 for versioned_sources in self.sources.values_mut() {
454 for (version, sources, (profile, _)) in versioned_sources {
455 trace!("Filtering {} sources for {}", sources.len(), version);
456 cache.filter(sources, version, profile);
457 trace!(
458 "Detected {} sources to compile {:?}",
459 sources.dirty().count(),
460 sources.dirty_files().collect::<Vec<_>>()
461 );
462 }
463 }
464 }
465
466 fn compile<
468 C: Compiler<Language = L, Settings = S>,
469 T: ArtifactOutput<CompilerContract = C::CompilerContract>,
470 >(
471 self,
472 cache: &mut ArtifactsCache<'_, T, C>,
473 preprocessor: Option<Box<dyn Preprocessor<C>>>,
474 ) -> Result<AggregatedCompilerOutput<C>> {
475 let project = cache.project();
476 let graph = cache.graph();
477
478 let jobs_cnt = self.jobs;
479
480 let sparse_output = SparseOutputFilter::new(project.sparse_output.as_deref());
481
482 let mut include_paths = project.paths.include_paths.clone();
484 include_paths.extend(graph.include_paths().clone());
485
486 let mut mocks = cache.mocks();
489
490 let mut jobs = Vec::new();
491 for (language, versioned_sources) in self.sources {
492 for (version, sources, (profile, opt_settings)) in versioned_sources {
493 let mut opt_settings = opt_settings.clone();
494 if sources.is_empty() {
495 trace!("skip {} for empty sources set", version);
497 continue;
498 }
499
500 let actually_dirty =
503 sparse_output.sparse_sources(&sources, &mut opt_settings, graph);
504
505 if actually_dirty.is_empty() {
506 trace!("skip {} run due to empty source set", version);
509 continue;
510 }
511
512 trace!("calling {} with {} sources {:?}", version, sources.len(), sources.keys());
513
514 let settings = opt_settings
515 .with_base_path(&project.paths.root)
516 .with_allow_paths(&project.paths.allowed_paths)
517 .with_include_paths(&include_paths)
518 .with_remappings(&project.paths.remappings);
519
520 let mut input = C::Input::build(sources, settings, language, version.clone());
521
522 input.strip_prefix(project.paths.root.as_path());
523
524 if let Some(preprocessor) = preprocessor.as_ref() {
525 preprocessor.preprocess(
526 &project.compiler,
527 &mut input,
528 &project.paths,
529 &mut mocks,
530 )?;
531 }
532
533 jobs.push((input, profile, actually_dirty));
534 }
535 }
536
537 cache.update_mocks(mocks);
539
540 let results = if let Some(num_jobs) = jobs_cnt {
541 compile_parallel(&project.compiler, jobs, num_jobs)
542 } else {
543 compile_sequential(&project.compiler, jobs)
544 }?;
545
546 let mut aggregated = AggregatedCompilerOutput::default();
547
548 for (input, mut output, profile, actually_dirty) in results {
549 let version = input.version();
550
551 for file in &actually_dirty {
553 cache.compiler_seen(file);
554 }
555
556 let build_info = RawBuildInfo::new(&input, &output, project.build_info)?;
557
558 output.retain_files(
559 actually_dirty
560 .iter()
561 .map(|f| f.strip_prefix(project.paths.root.as_path()).unwrap_or(f)),
562 );
563 output.join_all(project.paths.root.as_path());
564
565 aggregated.extend(version.clone(), build_info, profile, output);
566 }
567
568 Ok(aggregated)
569 }
570}
571
572type CompilationResult<'a, I, E, C> = Result<Vec<(I, CompilerOutput<E, C>, &'a str, Vec<PathBuf>)>>;
573
574fn compile_sequential<'a, C: Compiler>(
576 compiler: &C,
577 jobs: Vec<(C::Input, &'a str, Vec<PathBuf>)>,
578) -> CompilationResult<'a, C::Input, C::CompilationError, C::CompilerContract> {
579 jobs.into_iter()
580 .map(|(input, profile, actually_dirty)| {
581 let start = Instant::now();
582 report::compiler_spawn(
583 &input.compiler_name(),
584 input.version(),
585 actually_dirty.as_slice(),
586 );
587 let output = compiler.compile(&input)?;
588 report::compiler_success(&input.compiler_name(), input.version(), &start.elapsed());
589
590 Ok((input, output, profile, actually_dirty))
591 })
592 .collect()
593}
594
595fn compile_parallel<'a, C: Compiler>(
597 compiler: &C,
598 jobs: Vec<(C::Input, &'a str, Vec<PathBuf>)>,
599 num_jobs: usize,
600) -> CompilationResult<'a, C::Input, C::CompilationError, C::CompilerContract> {
601 let scoped_report = report::get_default(|reporter| reporter.clone());
605
606 let pool = rayon::ThreadPoolBuilder::new().num_threads(num_jobs).build().unwrap();
608
609 pool.install(move || {
610 jobs.into_par_iter()
611 .map(move |(input, profile, actually_dirty)| {
612 let _guard = report::set_scoped(&scoped_report);
614
615 let start = Instant::now();
616 report::compiler_spawn(
617 &input.compiler_name(),
618 input.version(),
619 actually_dirty.as_slice(),
620 );
621 compiler.compile(&input).map(move |output| {
622 report::compiler_success(
623 &input.compiler_name(),
624 input.version(),
625 &start.elapsed(),
626 );
627 (input, output, profile, actually_dirty)
628 })
629 })
630 .collect()
631 })
632}
633
634#[cfg(test)]
635#[cfg(all(feature = "project-util", feature = "svm-solc"))]
636mod tests {
637 use std::path::Path;
638
639 use foundry_compilers_artifacts::output_selection::ContractOutputSelection;
640
641 use crate::{
642 compilers::multi::MultiCompiler, project_util::TempProject, ConfigurableArtifacts,
643 MinimalCombinedArtifacts, ProjectPathsConfig,
644 };
645
646 use super::*;
647
648 fn init_tracing() {
649 let _ = tracing_subscriber::fmt()
650 .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
651 .try_init()
652 .ok();
653 }
654
655 #[test]
656 fn can_preprocess() {
657 let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../test-data/dapp-sample");
658 let project = Project::builder()
659 .paths(ProjectPathsConfig::dapptools(&root).unwrap())
660 .build(Default::default())
661 .unwrap();
662
663 let compiler = ProjectCompiler::new(&project).unwrap();
664 let prep = compiler.preprocess().unwrap();
665 let cache = prep.cache.as_cached().unwrap();
666 assert_eq!(cache.cache.files.len(), 3);
668 assert!(cache.cache.files.values().all(|v| v.artifacts.is_empty()));
669
670 let compiled = prep.compile().unwrap();
671 assert_eq!(compiled.output.contracts.files().count(), 3);
672 }
673
674 #[test]
675 fn can_detect_cached_files() {
676 let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../test-data/dapp-sample");
677 let paths = ProjectPathsConfig::builder().sources(root.join("src")).lib(root.join("lib"));
678 let project = TempProject::<MultiCompiler, MinimalCombinedArtifacts>::new(paths).unwrap();
679
680 let compiled = project.compile().unwrap();
681 compiled.assert_success();
682
683 let inner = project.project();
684 let compiler = ProjectCompiler::new(inner).unwrap();
685 let prep = compiler.preprocess().unwrap();
686 assert!(prep.cache.as_cached().unwrap().dirty_sources.is_empty())
687 }
688
689 #[test]
690 fn can_recompile_with_optimized_output() {
691 let tmp = TempProject::<MultiCompiler, ConfigurableArtifacts>::dapptools().unwrap();
692
693 tmp.add_source(
694 "A",
695 r#"
696 pragma solidity ^0.8.10;
697 import "./B.sol";
698 contract A {}
699 "#,
700 )
701 .unwrap();
702
703 tmp.add_source(
704 "B",
705 r#"
706 pragma solidity ^0.8.10;
707 contract B {
708 function hello() public {}
709 }
710 import "./C.sol";
711 "#,
712 )
713 .unwrap();
714
715 tmp.add_source(
716 "C",
717 r"
718 pragma solidity ^0.8.10;
719 contract C {
720 function hello() public {}
721 }
722 ",
723 )
724 .unwrap();
725 let compiled = tmp.compile().unwrap();
726 compiled.assert_success();
727
728 tmp.artifacts_snapshot().unwrap().assert_artifacts_essentials_present();
729
730 tmp.add_source(
732 "A",
733 r#"
734 pragma solidity ^0.8.10;
735 import "./B.sol";
736 contract A {
737 function testExample() public {}
738 }
739 "#,
740 )
741 .unwrap();
742
743 let compiler = ProjectCompiler::new(tmp.project()).unwrap();
744 let state = compiler.preprocess().unwrap();
745 let sources = &state.sources.sources;
746
747 let cache = state.cache.as_cached().unwrap();
748
749 assert_eq!(cache.cache.artifacts_len(), 2);
751 assert!(cache.cache.all_artifacts_exist());
752 assert_eq!(cache.dirty_sources.len(), 1);
753
754 let len = sources.values().map(|v| v.len()).sum::<usize>();
755 assert_eq!(len, 1);
757
758 let filtered = &sources.values().next().unwrap()[0].1;
759
760 assert_eq!(filtered.0.len(), 3);
762 assert_eq!(filtered.dirty().count(), 1);
764 assert!(filtered.dirty_files().next().unwrap().ends_with("A.sol"));
765
766 let state = state.compile().unwrap();
767 assert_eq!(state.output.sources.len(), 1);
768 for (f, source) in state.output.sources.sources() {
769 if f.ends_with("A.sol") {
770 assert!(source.ast.is_some());
771 } else {
772 assert!(source.ast.is_none());
773 }
774 }
775
776 assert_eq!(state.output.contracts.len(), 1);
777 let (a, c) = state.output.contracts_iter().next().unwrap();
778 assert_eq!(a, "A");
779 assert!(c.abi.is_some() && c.evm.is_some());
780
781 let state = state.write_artifacts().unwrap();
782 assert_eq!(state.compiled_artifacts.as_ref().len(), 1);
783
784 let out = state.write_cache().unwrap();
785
786 let artifacts: Vec<_> = out.into_artifacts().collect();
787 assert_eq!(artifacts.len(), 3);
788 for (_, artifact) in artifacts {
789 let c = artifact.into_contract_bytecode();
790 assert!(c.abi.is_some() && c.bytecode.is_some() && c.deployed_bytecode.is_some());
791 }
792
793 tmp.artifacts_snapshot().unwrap().assert_artifacts_essentials_present();
794 }
795
796 #[test]
797 #[ignore]
798 fn can_compile_real_project() {
799 init_tracing();
800 let paths = ProjectPathsConfig::builder()
801 .root("../../foundry-integration-tests/testdata/solmate")
802 .build()
803 .unwrap();
804 let project = Project::builder().paths(paths).build(Default::default()).unwrap();
805 let compiler = ProjectCompiler::new(&project).unwrap();
806 let _out = compiler.compile().unwrap();
807 }
808
809 #[test]
810 fn extra_output_cached() {
811 let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../test-data/dapp-sample");
812 let paths = ProjectPathsConfig::builder().sources(root.join("src")).lib(root.join("lib"));
813 let mut project = TempProject::<MultiCompiler>::new(paths).unwrap();
814
815 project.compile().unwrap();
817
818 project.project_mut().artifacts =
820 ConfigurableArtifacts::new([], [ContractOutputSelection::Abi]);
821
822 let abi_path = project.project().paths.artifacts.join("Dapp.sol/Dapp.abi.json");
824 assert!(!abi_path.exists());
825 let output = project.compile().unwrap();
826 assert!(output.compiler_output.is_empty());
827 assert!(abi_path.exists());
828 }
829
830 #[test]
831 fn can_compile_leftovers_after_sparse() {
832 let mut tmp = TempProject::<MultiCompiler, ConfigurableArtifacts>::dapptools().unwrap();
833
834 tmp.add_source(
835 "A",
836 r#"
837pragma solidity ^0.8.10;
838import "./B.sol";
839contract A {}
840"#,
841 )
842 .unwrap();
843
844 tmp.add_source(
845 "B",
846 r#"
847pragma solidity ^0.8.10;
848contract B {}
849"#,
850 )
851 .unwrap();
852
853 tmp.project_mut().sparse_output = Some(Box::new(|f: &Path| f.ends_with("A.sol")));
854 let compiled = tmp.compile().unwrap();
855 compiled.assert_success();
856 assert_eq!(compiled.artifacts().count(), 1);
857
858 tmp.project_mut().sparse_output = None;
859 let compiled = tmp.compile().unwrap();
860 compiled.assert_success();
861 assert_eq!(compiled.artifacts().count(), 2);
862 }
863}