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