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, Sources,
113};
114use foundry_compilers_core::error::Result;
115use rayon::prelude::*;
116use semver::Version;
117use std::{collections::HashMap, path::PathBuf, time::Instant};
118
119pub(crate) type VersionedSources<'a, L, S> = HashMap<L, Vec<(Version, Sources, (&'a str, &'a S))>>;
121
122#[derive(Debug)]
123pub struct ProjectCompiler<
124 'a,
125 T: ArtifactOutput<CompilerContract = C::CompilerContract>,
126 C: Compiler,
127> {
128 edges: GraphEdges<C::ParsedSource>,
130 project: &'a Project<C, T>,
131 primary_profiles: HashMap<PathBuf, &'a str>,
133 sources: CompilerSources<'a, C::Language, C::Settings>,
135}
136
137impl<'a, T: ArtifactOutput<CompilerContract = C::CompilerContract>, C: Compiler>
138 ProjectCompiler<'a, T, C>
139{
140 pub fn new(project: &'a Project<C, T>) -> Result<Self> {
143 Self::with_sources(project, project.paths.read_input_files()?)
144 }
145
146 pub fn with_sources(project: &'a Project<C, T>, mut sources: Sources) -> Result<Self> {
153 if let Some(filter) = &project.sparse_output {
154 sources.retain(|f, _| filter.is_match(f))
155 }
156 let graph = Graph::resolve_sources(&project.paths, sources)?;
157 let ResolvedSources { sources, primary_profiles, edges } =
158 graph.into_sources_by_version(project)?;
159
160 let jobs_cnt = || sources.values().map(|v| v.len()).sum::<usize>();
163 let sources = CompilerSources {
164 jobs: (project.solc_jobs > 1 && jobs_cnt() > 1).then_some(project.solc_jobs),
165 sources,
166 };
167
168 Ok(Self { edges, primary_profiles, project, sources })
169 }
170
171 pub fn compile(self) -> Result<ProjectCompileOutput<C, T>> {
187 let slash_paths = self.project.slash_paths;
188
189 let mut output = self.preprocess()?.compile()?.write_artifacts()?.write_cache()?;
191
192 if slash_paths {
193 output.slash_paths();
195 }
196
197 Ok(output)
198 }
199
200 fn preprocess(self) -> Result<PreprocessedState<'a, T, C>> {
204 trace!("preprocessing");
205 let Self { edges, project, mut sources, primary_profiles } = self;
206
207 sources.slash_paths();
210
211 let mut cache = ArtifactsCache::new(project, edges)?;
212 sources.filter(&mut cache);
214
215 Ok(PreprocessedState { sources, cache, primary_profiles })
216 }
217}
218
219#[derive(Debug)]
223struct PreprocessedState<'a, T: ArtifactOutput<CompilerContract = C::CompilerContract>, C: Compiler>
224{
225 sources: CompilerSources<'a, C::Language, C::Settings>,
227
228 cache: ArtifactsCache<'a, T, C>,
230
231 primary_profiles: HashMap<PathBuf, &'a str>,
233}
234
235impl<'a, T: ArtifactOutput<CompilerContract = C::CompilerContract>, C: Compiler>
236 PreprocessedState<'a, T, C>
237{
238 fn compile(self) -> Result<CompiledState<'a, T, C>> {
240 trace!("compiling");
241 let PreprocessedState { sources, mut cache, primary_profiles } = self;
242
243 let mut output = sources.compile(&mut cache)?;
244
245 output.join_all(cache.project().root());
251
252 Ok(CompiledState { output, cache, primary_profiles })
253 }
254}
255
256#[derive(Debug)]
258struct CompiledState<'a, T: ArtifactOutput<CompilerContract = C::CompilerContract>, C: Compiler> {
259 output: AggregatedCompilerOutput<C>,
260 cache: ArtifactsCache<'a, T, C>,
261 primary_profiles: HashMap<PathBuf, &'a str>,
262}
263
264impl<'a, T: ArtifactOutput<CompilerContract = C::CompilerContract>, C: Compiler>
265 CompiledState<'a, T, C>
266{
267 #[instrument(skip_all, name = "write-artifacts")]
272 fn write_artifacts(self) -> Result<ArtifactsState<'a, T, C>> {
273 let CompiledState { output, cache, primary_profiles } = self;
274
275 let project = cache.project();
276 let ctx = cache.output_ctx();
277 let compiled_artifacts = if project.no_artifacts {
280 project.artifacts_handler().output_to_artifacts(
281 &output.contracts,
282 &output.sources,
283 ctx,
284 &project.paths,
285 &primary_profiles,
286 )
287 } else if output.has_error(
288 &project.ignored_error_codes,
289 &project.ignored_file_paths,
290 &project.compiler_severity_filter,
291 ) {
292 trace!("skip writing cache file due to solc errors: {:?}", output.errors);
293 project.artifacts_handler().output_to_artifacts(
294 &output.contracts,
295 &output.sources,
296 ctx,
297 &project.paths,
298 &primary_profiles,
299 )
300 } else {
301 trace!(
302 "handling artifact output for {} contracts and {} sources",
303 output.contracts.len(),
304 output.sources.len()
305 );
306 let artifacts = project.artifacts_handler().on_output(
308 &output.contracts,
309 &output.sources,
310 &project.paths,
311 ctx,
312 &primary_profiles,
313 )?;
314
315 output.write_build_infos(project.build_info_path())?;
317
318 artifacts
319 };
320
321 Ok(ArtifactsState { output, cache, compiled_artifacts })
322 }
323}
324
325#[derive(Debug)]
327struct ArtifactsState<'a, T: ArtifactOutput<CompilerContract = C::CompilerContract>, C: Compiler> {
328 output: AggregatedCompilerOutput<C>,
329 cache: ArtifactsCache<'a, T, C>,
330 compiled_artifacts: Artifacts<T::Artifact>,
331}
332
333impl<T: ArtifactOutput<CompilerContract = C::CompilerContract>, C: Compiler>
334 ArtifactsState<'_, T, C>
335{
336 fn write_cache(self) -> Result<ProjectCompileOutput<C, T>> {
340 let ArtifactsState { output, cache, compiled_artifacts } = self;
341 let project = cache.project();
342 let ignored_error_codes = project.ignored_error_codes.clone();
343 let ignored_file_paths = project.ignored_file_paths.clone();
344 let compiler_severity_filter = project.compiler_severity_filter;
345 let has_error =
346 output.has_error(&ignored_error_codes, &ignored_file_paths, &compiler_severity_filter);
347 let skip_write_to_disk = project.no_artifacts || has_error;
348 trace!(has_error, project.no_artifacts, skip_write_to_disk, cache_path=?project.cache_path(),"prepare writing cache file");
349
350 let (cached_artifacts, cached_builds) =
351 cache.consume(&compiled_artifacts, &output.build_infos, !skip_write_to_disk)?;
352
353 project.artifacts_handler().handle_cached_artifacts(&cached_artifacts)?;
354
355 let builds = Builds(
356 output
357 .build_infos
358 .iter()
359 .map(|build_info| (build_info.id.clone(), build_info.build_context.clone()))
360 .chain(cached_builds)
361 .map(|(id, context)| (id, context.with_joined_paths(project.paths.root.as_path())))
362 .collect(),
363 );
364
365 Ok(ProjectCompileOutput {
366 compiler_output: output,
367 compiled_artifacts,
368 cached_artifacts,
369 ignored_error_codes,
370 ignored_file_paths,
371 compiler_severity_filter,
372 builds,
373 })
374 }
375}
376
377#[derive(Debug, Clone)]
379struct CompilerSources<'a, L, S> {
380 sources: VersionedSources<'a, L, S>,
382 jobs: Option<usize>,
384}
385
386impl<L: Language, S: CompilerSettings> CompilerSources<'_, L, S> {
387 fn slash_paths(&mut self) {
392 #[cfg(windows)]
393 {
394 use path_slash::PathBufExt;
395
396 self.sources.values_mut().for_each(|versioned_sources| {
397 versioned_sources.iter_mut().for_each(|(_, sources, _)| {
398 *sources = std::mem::take(sources)
399 .into_iter()
400 .map(|(path, source)| {
401 (PathBuf::from(path.to_slash_lossy().as_ref()), source)
402 })
403 .collect()
404 })
405 });
406 }
407 }
408
409 fn filter<
411 T: ArtifactOutput<CompilerContract = C::CompilerContract>,
412 C: Compiler<Language = L>,
413 >(
414 &mut self,
415 cache: &mut ArtifactsCache<'_, T, C>,
416 ) {
417 cache.remove_dirty_sources();
418 for versioned_sources in self.sources.values_mut() {
419 for (version, sources, (profile, _)) in versioned_sources {
420 trace!("Filtering {} sources for {}", sources.len(), version);
421 cache.filter(sources, version, profile);
422 trace!(
423 "Detected {} sources to compile {:?}",
424 sources.dirty().count(),
425 sources.dirty_files().collect::<Vec<_>>()
426 );
427 }
428 }
429 }
430
431 fn compile<
433 C: Compiler<Language = L, Settings = S>,
434 T: ArtifactOutput<CompilerContract = C::CompilerContract>,
435 >(
436 self,
437 cache: &mut ArtifactsCache<'_, T, C>,
438 ) -> Result<AggregatedCompilerOutput<C>> {
439 let project = cache.project();
440 let graph = cache.graph();
441
442 let jobs_cnt = self.jobs;
443
444 let sparse_output = SparseOutputFilter::new(project.sparse_output.as_deref());
445
446 let mut include_paths = project.paths.include_paths.clone();
448 include_paths.extend(graph.include_paths().clone());
449
450 let mut jobs = Vec::new();
451 for (language, versioned_sources) in self.sources {
452 for (version, sources, (profile, opt_settings)) in versioned_sources {
453 let mut opt_settings = opt_settings.clone();
454 if sources.is_empty() {
455 trace!("skip {} for empty sources set", version);
457 continue;
458 }
459
460 let actually_dirty =
463 sparse_output.sparse_sources(&sources, &mut opt_settings, graph);
464
465 if actually_dirty.is_empty() {
466 trace!("skip {} run due to empty source set", version);
469 continue;
470 }
471
472 trace!("calling {} with {} sources {:?}", version, sources.len(), sources.keys());
473
474 let settings = opt_settings
475 .with_base_path(&project.paths.root)
476 .with_allow_paths(&project.paths.allowed_paths)
477 .with_include_paths(&include_paths)
478 .with_remappings(&project.paths.remappings);
479
480 let mut input = C::Input::build(sources, settings, language, version.clone());
481
482 input.strip_prefix(project.paths.root.as_path());
483
484 jobs.push((input, profile, actually_dirty));
485 }
486 }
487
488 let results = if let Some(num_jobs) = jobs_cnt {
489 compile_parallel(&project.compiler, jobs, num_jobs)
490 } else {
491 compile_sequential(&project.compiler, jobs)
492 }?;
493
494 let mut aggregated = AggregatedCompilerOutput::default();
495
496 for (input, mut output, profile, actually_dirty) in results {
497 let version = input.version();
498
499 for file in &actually_dirty {
501 cache.compiler_seen(file);
502 }
503
504 let build_info = RawBuildInfo::new(&input, &output, project.build_info)?;
505
506 output.retain_files(
507 actually_dirty
508 .iter()
509 .map(|f| f.strip_prefix(project.paths.root.as_path()).unwrap_or(f)),
510 );
511 output.join_all(project.paths.root.as_path());
512
513 aggregated.extend(version.clone(), build_info, profile, output);
514 }
515
516 Ok(aggregated)
517 }
518}
519
520type CompilationResult<'a, I, E, C> = Result<Vec<(I, CompilerOutput<E, C>, &'a str, Vec<PathBuf>)>>;
521
522fn compile_sequential<'a, C: Compiler>(
524 compiler: &C,
525 jobs: Vec<(C::Input, &'a str, Vec<PathBuf>)>,
526) -> CompilationResult<'a, C::Input, C::CompilationError, C::CompilerContract> {
527 jobs.into_iter()
528 .map(|(input, profile, actually_dirty)| {
529 let start = Instant::now();
530 report::compiler_spawn(
531 &input.compiler_name(),
532 input.version(),
533 actually_dirty.as_slice(),
534 );
535 let output = compiler.compile(&input)?;
536 report::compiler_success(&input.compiler_name(), input.version(), &start.elapsed());
537
538 Ok((input, output, profile, actually_dirty))
539 })
540 .collect()
541}
542
543fn compile_parallel<'a, C: Compiler>(
545 compiler: &C,
546 jobs: Vec<(C::Input, &'a str, Vec<PathBuf>)>,
547 num_jobs: usize,
548) -> CompilationResult<'a, C::Input, C::CompilationError, C::CompilerContract> {
549 let scoped_report = report::get_default(|reporter| reporter.clone());
553
554 let pool = rayon::ThreadPoolBuilder::new().num_threads(num_jobs).build().unwrap();
556
557 pool.install(move || {
558 jobs.into_par_iter()
559 .map(move |(input, profile, actually_dirty)| {
560 let _guard = report::set_scoped(&scoped_report);
562
563 let start = Instant::now();
564 report::compiler_spawn(
565 &input.compiler_name(),
566 input.version(),
567 actually_dirty.as_slice(),
568 );
569 compiler.compile(&input).map(move |output| {
570 report::compiler_success(
571 &input.compiler_name(),
572 input.version(),
573 &start.elapsed(),
574 );
575 (input, output, profile, actually_dirty)
576 })
577 })
578 .collect()
579 })
580}
581
582#[cfg(test)]
583#[cfg(all(feature = "project-util", feature = "svm-solc"))]
584mod tests {
585 use std::path::Path;
586
587 use foundry_compilers_artifacts::output_selection::ContractOutputSelection;
588
589 use crate::{
590 compilers::multi::MultiCompiler, project_util::TempProject, ConfigurableArtifacts,
591 MinimalCombinedArtifacts, ProjectPathsConfig,
592 };
593
594 use super::*;
595
596 fn init_tracing() {
597 let _ = tracing_subscriber::fmt()
598 .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
599 .try_init()
600 .ok();
601 }
602
603 #[test]
604 fn can_preprocess() {
605 let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../test-data/dapp-sample");
606 let project = Project::builder()
607 .paths(ProjectPathsConfig::dapptools(&root).unwrap())
608 .build(Default::default())
609 .unwrap();
610
611 let compiler = ProjectCompiler::new(&project).unwrap();
612 let prep = compiler.preprocess().unwrap();
613 let cache = prep.cache.as_cached().unwrap();
614 assert_eq!(cache.cache.files.len(), 3);
616 assert!(cache.cache.files.values().all(|v| v.artifacts.is_empty()));
617
618 let compiled = prep.compile().unwrap();
619 assert_eq!(compiled.output.contracts.files().count(), 3);
620 }
621
622 #[test]
623 fn can_detect_cached_files() {
624 let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../test-data/dapp-sample");
625 let paths = ProjectPathsConfig::builder().sources(root.join("src")).lib(root.join("lib"));
626 let project = TempProject::<MultiCompiler, MinimalCombinedArtifacts>::new(paths).unwrap();
627
628 let compiled = project.compile().unwrap();
629 compiled.assert_success();
630
631 let inner = project.project();
632 let compiler = ProjectCompiler::new(inner).unwrap();
633 let prep = compiler.preprocess().unwrap();
634 assert!(prep.cache.as_cached().unwrap().dirty_sources.is_empty())
635 }
636
637 #[test]
638 fn can_recompile_with_optimized_output() {
639 let tmp = TempProject::<MultiCompiler, ConfigurableArtifacts>::dapptools().unwrap();
640
641 tmp.add_source(
642 "A",
643 r#"
644 pragma solidity ^0.8.10;
645 import "./B.sol";
646 contract A {}
647 "#,
648 )
649 .unwrap();
650
651 tmp.add_source(
652 "B",
653 r#"
654 pragma solidity ^0.8.10;
655 contract B {
656 function hello() public {}
657 }
658 import "./C.sol";
659 "#,
660 )
661 .unwrap();
662
663 tmp.add_source(
664 "C",
665 r"
666 pragma solidity ^0.8.10;
667 contract C {
668 function hello() public {}
669 }
670 ",
671 )
672 .unwrap();
673 let compiled = tmp.compile().unwrap();
674 compiled.assert_success();
675
676 tmp.artifacts_snapshot().unwrap().assert_artifacts_essentials_present();
677
678 tmp.add_source(
680 "A",
681 r#"
682 pragma solidity ^0.8.10;
683 import "./B.sol";
684 contract A {
685 function testExample() public {}
686 }
687 "#,
688 )
689 .unwrap();
690
691 let compiler = ProjectCompiler::new(tmp.project()).unwrap();
692 let state = compiler.preprocess().unwrap();
693 let sources = &state.sources.sources;
694
695 let cache = state.cache.as_cached().unwrap();
696
697 assert_eq!(cache.cache.artifacts_len(), 2);
699 assert!(cache.cache.all_artifacts_exist());
700 assert_eq!(cache.dirty_sources.len(), 1);
701
702 let len = sources.values().map(|v| v.len()).sum::<usize>();
703 assert_eq!(len, 1);
705
706 let filtered = &sources.values().next().unwrap()[0].1;
707
708 assert_eq!(filtered.0.len(), 3);
710 assert_eq!(filtered.dirty().count(), 1);
712 assert!(filtered.dirty_files().next().unwrap().ends_with("A.sol"));
713
714 let state = state.compile().unwrap();
715 assert_eq!(state.output.sources.len(), 1);
716 for (f, source) in state.output.sources.sources() {
717 if f.ends_with("A.sol") {
718 assert!(source.ast.is_some());
719 } else {
720 assert!(source.ast.is_none());
721 }
722 }
723
724 assert_eq!(state.output.contracts.len(), 1);
725 let (a, c) = state.output.contracts_iter().next().unwrap();
726 assert_eq!(a, "A");
727 assert!(c.abi.is_some() && c.evm.is_some());
728
729 let state = state.write_artifacts().unwrap();
730 assert_eq!(state.compiled_artifacts.as_ref().len(), 1);
731
732 let out = state.write_cache().unwrap();
733
734 let artifacts: Vec<_> = out.into_artifacts().collect();
735 assert_eq!(artifacts.len(), 3);
736 for (_, artifact) in artifacts {
737 let c = artifact.into_contract_bytecode();
738 assert!(c.abi.is_some() && c.bytecode.is_some() && c.deployed_bytecode.is_some());
739 }
740
741 tmp.artifacts_snapshot().unwrap().assert_artifacts_essentials_present();
742 }
743
744 #[test]
745 #[ignore]
746 fn can_compile_real_project() {
747 init_tracing();
748 let paths = ProjectPathsConfig::builder()
749 .root("../../foundry-integration-tests/testdata/solmate")
750 .build()
751 .unwrap();
752 let project = Project::builder().paths(paths).build(Default::default()).unwrap();
753 let compiler = ProjectCompiler::new(&project).unwrap();
754 let _out = compiler.compile().unwrap();
755 }
756
757 #[test]
758 fn extra_output_cached() {
759 let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../test-data/dapp-sample");
760 let paths = ProjectPathsConfig::builder().sources(root.join("src")).lib(root.join("lib"));
761 let mut project = TempProject::<MultiCompiler>::new(paths).unwrap();
762
763 project.compile().unwrap();
765
766 project.project_mut().artifacts =
768 ConfigurableArtifacts::new([], [ContractOutputSelection::Abi]);
769
770 let abi_path = project.project().paths.artifacts.join("Dapp.sol/Dapp.abi.json");
772 assert!(!abi_path.exists());
773 let output = project.compile().unwrap();
774 assert!(output.compiler_output.is_empty());
775 assert!(abi_path.exists());
776 }
777
778 #[test]
779 fn can_compile_leftovers_after_sparse() {
780 let mut tmp = TempProject::<MultiCompiler, ConfigurableArtifacts>::dapptools().unwrap();
781
782 tmp.add_source(
783 "A",
784 r#"
785pragma solidity ^0.8.10;
786import "./B.sol";
787contract A {}
788"#,
789 )
790 .unwrap();
791
792 tmp.add_source(
793 "B",
794 r#"
795pragma solidity ^0.8.10;
796contract B {}
797"#,
798 )
799 .unwrap();
800
801 tmp.project_mut().sparse_output = Some(Box::new(|f: &Path| f.ends_with("A.sol")));
802 let compiled = tmp.compile().unwrap();
803 compiled.assert_success();
804 assert_eq!(compiled.artifacts().count(), 1);
805
806 tmp.project_mut().sparse_output = None;
807 let compiled = tmp.compile().unwrap();
808 compiled.assert_success();
809 assert_eq!(compiled.artifacts().count(), 2);
810 }
811}