1use crate::{
4 TestFunctionExt,
5 external_compiler::{ExternalCompilation, is_builtin_compiler_source},
6 fs::canonicalize_path,
7 preprocessor::DynamicTestLinkingPreprocessor,
8 shell,
9 term::SpinnerReporter,
10};
11use alloy_json_abi::JsonAbi;
12use comfy_table::{
13 Cell, Color, Table,
14 presets::{ASCII_FULL, ASCII_MARKDOWN},
15};
16use eyre::{OptionExt, Result};
17use foundry_block_explorers::contract::Metadata;
18use foundry_compilers::{
19 Artifact, Project, ProjectBuilder, ProjectCompileOutput, ProjectPathsConfig, SolcConfig,
20 artifacts::{
21 BytecodeObject, Contract, Error as SolcError, Severity, Source,
22 output_selection::OutputSelection, remappings::Remapping,
23 },
24 compilers::{
25 CompilationError, Compiler,
26 solc::{Solc, SolcCompiler},
27 },
28 info::ContractInfo as CompilerContractInfo,
29 multi::{MultiCompiler, MultiCompilerError, MultiCompilerSettings},
30 project::Preprocessor,
31 report::{BasicStdoutReporter, NoReporter, Report},
32 solc::SolcSettings,
33};
34use foundry_config::Config;
35use num_format::{Locale, ToFormattedString};
36use revm::primitives::{eip170, eip3860, hardfork::SpecId};
37use solar::{
38 ast::{Arena, ContractKind, ItemKind},
39 interface::{Session, source_map::FileName},
40 parse::Parser,
41};
42use std::{
43 any::Any,
44 collections::{BTreeMap, BTreeSet, HashMap, HashSet},
45 fmt::Display,
46 io::IsTerminal,
47 path::{Component, Path, PathBuf},
48 str::FromStr,
49 sync::Arc,
50 time::Instant,
51};
52
53pub type Analysis = Arc<solar::sema::Compiler>;
55
56#[must_use = "ProjectCompiler does nothing unless you call a `compile*` method"]
61pub struct ProjectCompiler {
62 project_root: PathBuf,
64
65 print_names: Option<bool>,
67
68 print_sizes: Option<bool>,
70
71 quiet: Option<bool>,
73
74 print_compiler_settings: bool,
76
77 bail: Option<bool>,
79
80 ignore_eip_3860: bool,
82
83 size_limits: ContractSizeLimits,
85
86 files: Vec<PathBuf>,
88
89 source_order_fallback: Vec<PathBuf>,
91
92 selected_paths: Vec<PathBuf>,
94
95 dynamic_test_linking: bool,
97
98 allow_linked_libraries: bool,
100
101 abi_cache: bool,
103
104 external_compilers: Option<Config>,
106
107 external_writes: bool,
110}
111
112impl Default for ProjectCompiler {
113 #[inline]
114 fn default() -> Self {
115 Self::new()
116 }
117}
118
119impl ProjectCompiler {
120 #[inline]
122 pub fn new() -> Self {
123 Self {
124 project_root: PathBuf::new(),
125 print_names: None,
126 print_sizes: None,
127 quiet: Some(crate::shell::is_quiet()),
128 print_compiler_settings: false,
129 bail: None,
130 ignore_eip_3860: false,
131 size_limits: ContractSizeLimits::default(),
132 files: Vec::new(),
133 source_order_fallback: Vec::new(),
134 selected_paths: Vec::new(),
135 dynamic_test_linking: false,
136 allow_linked_libraries: true,
137 abi_cache: false,
138 external_compilers: None,
139 external_writes: true,
140 }
141 }
142
143 #[inline]
145 pub const fn print_names(mut self, yes: bool) -> Self {
146 self.print_names = Some(yes);
147 self
148 }
149
150 #[inline]
152 pub const fn print_sizes(mut self, yes: bool) -> Self {
153 self.print_sizes = Some(yes);
154 self
155 }
156
157 #[inline]
159 #[doc(alias = "silent")]
160 pub const fn quiet(mut self, yes: bool) -> Self {
161 self.quiet = Some(yes);
162 self
163 }
164
165 #[inline]
167 pub const fn print_compiler_settings(mut self, yes: bool) -> Self {
168 self.print_compiler_settings = yes;
169 self
170 }
171
172 #[inline]
174 pub const fn bail(mut self, yes: bool) -> Self {
175 self.bail = Some(yes);
176 self
177 }
178
179 #[inline]
181 pub const fn ignore_eip_3860(mut self, yes: bool) -> Self {
182 self.ignore_eip_3860 = yes;
183 self
184 }
185
186 #[inline]
188 pub const fn size_limits(mut self, limits: ContractSizeLimits) -> Self {
189 self.size_limits = limits;
190 self
191 }
192
193 #[inline]
195 pub fn files(mut self, files: impl IntoIterator<Item = PathBuf>) -> Self {
196 self.files.extend(files);
197 self
198 }
199
200 pub fn selected_paths(mut self, paths: impl IntoIterator<Item = PathBuf>) -> Self {
202 self.selected_paths.extend(paths);
203 self
204 }
205
206 pub fn target_files(mut self, paths: impl IntoIterator<Item = PathBuf>) -> Self {
208 for path in paths {
209 if is_builtin_compiler_source(&path) {
210 self.files.push(path.clone());
211 }
212 self.selected_paths.push(path);
213 }
214 self
215 }
216
217 #[inline]
219 pub const fn dynamic_test_linking(mut self, preprocess: bool) -> Self {
220 self.dynamic_test_linking = preprocess;
221 self
222 }
223
224 pub fn external_compilers(mut self, config: &Config) -> Self {
226 self.external_compilers = Some(config.clone());
227 self
228 }
229
230 pub const fn external_artifacts(mut self, write: bool) -> Self {
232 self.external_writes = write;
233 self
234 }
235
236 pub const fn cache_abi(mut self) -> Self {
241 self.abi_cache = true;
242 self
243 }
244
245 pub fn source_order_fallback(mut self, files: impl IntoIterator<Item = PathBuf>) -> Self {
248 self.source_order_fallback.extend(files);
249 self
250 }
251
252 pub const fn allow_linked_libraries(mut self, allow: bool) -> Self {
257 self.allow_linked_libraries = allow;
258 self
259 }
260
261 #[instrument(target = "forge::compile", skip_all)]
263 pub fn compile<C: Compiler<CompilerContract = Contract>>(
264 mut self,
265 project: &Project<C>,
266 ) -> Result<ProjectCompileOutput<C>>
267 where
268 DynamicTestLinkingPreprocessor: Preprocessor<C>,
269 {
270 self.project_root = project.root().to_path_buf();
271
272 if !project.paths.has_input_files()
279 && self.files.is_empty()
280 && self.selected_paths.is_empty()
281 && self
282 .external_compilers
283 .as_ref()
284 .is_none_or(|config| config.external_compilers.is_empty())
285 {
286 sh_println!("Nothing to compile")?;
287 std::process::exit(0);
288 }
289
290 let files = std::mem::take(&mut self.files);
292 let fallback_files = std::mem::take(&mut self.source_order_fallback);
293 let explicit_selection = !files.is_empty() || !self.selected_paths.is_empty();
294 let selected_paths = std::mem::take(&mut self.selected_paths);
295 let preprocess = self.dynamic_test_linking;
296 let abi_cache = self.abi_cache;
297 let external_compilers = self.external_compilers.take();
298 let external_writes = self.external_writes && !project.no_artifacts;
299 self.compile_with(|| {
300 let external = external_compilers
301 .as_ref()
302 .map(|config| {
303 ExternalCompilation::compile(config, &selected_paths, external_writes)
304 })
305 .transpose()?;
306 let sources = if explicit_selection {
307 Source::read_all(files)?
308 } else {
309 project.paths.read_input_files()?
310 };
311
312 let preprocessor = DynamicTestLinkingPreprocessor::default();
313 let compile = |preprocess| -> Result<ProjectCompileOutput<C>> {
314 let compile_sources = |sources| {
315 let mut compiler = foundry_compilers::project::ProjectCompiler::with_sources(
316 project, sources,
317 )?;
318 if preprocess {
319 compiler = compiler.with_preprocessor(preprocessor.clone());
320 }
321 if abi_cache { compiler.compile_abi_cached() } else { compiler.compile() }
322 };
323 let mut output = compile_sources(sources.clone())?;
324 if !fallback_files.is_empty()
327 && output.output().errors.iter().any(|error| error.error_code() == Some(2449))
328 {
329 output = compile_sources(Source::read_all(&fallback_files)?)?;
330 }
331 Ok(output)
332 };
333 let mut output = compile(preprocess)?;
334 if preprocessor.rewrote_sources()
336 && let Some(reason) = dynamic_test_linking_failure(&output.output().errors)
337 {
338 sh_warn!(
339 "dynamic test linking failed ({reason}); retrying with native test linking. \
340 Use --no-dynamic-test-linking to skip the failed attempt."
341 )?;
342 output = compile(false)?;
345 }
346 if !output.has_compiler_errors()
347 && let Some(external) = external
348 {
349 external.merge(&mut output)?;
350 }
351 Ok(output)
352 })
353 }
354
355 fn compile_with<C: Compiler<CompilerContract = Contract>, F>(
357 self,
358 f: F,
359 ) -> Result<ProjectCompileOutput<C>>
360 where
361 F: FnOnce() -> Result<ProjectCompileOutput<C>>,
362 {
363 let quiet = self.quiet.unwrap_or(false);
364 let bail = self.bail.unwrap_or(true);
365
366 let mut output = with_compilation_reporter_and_settings(
367 quiet,
368 Some(self.project_root.clone()),
369 self.print_compiler_settings,
370 || {
371 tracing::debug!("compiling project");
372
373 let timer = Instant::now();
374 let r = f();
375 let elapsed = timer.elapsed();
376
377 tracing::debug!("finished compiling in {:.3}s", elapsed.as_secs_f64());
378 r
379 },
380 )?;
381
382 if bail && output.has_compiler_errors() {
383 eyre::bail!("{output}");
384 }
385
386 let mut linking_error = (!self.allow_linked_libraries && !output.has_compiler_errors())
387 .then(|| linked_libraries_error(&output, &self.project_root))
388 .flatten();
389 if !bail && let Some(message) = linking_error.take() {
390 push_linked_libraries_error(&mut output, message);
391 }
392
393 if !quiet && !shell::is_json() {
394 if output.is_unchanged() {
395 sh_println!("No files changed, compilation skipped")?;
396 } else {
397 sh_println!("{output}")?;
399 }
400 }
401
402 if let Some(message) = linking_error {
404 eyre::bail!("{message}");
405 }
406
407 if !(shell::is_json() && output.has_compiler_errors()) {
409 self.handle_output(&output)?;
410 }
411
412 Ok(output)
413 }
414
415 fn handle_output<C: Compiler<CompilerContract = Contract>>(
417 &self,
418 output: &ProjectCompileOutput<C>,
419 ) -> Result<()> {
420 let quiet = self.quiet.unwrap_or(false);
421 let print_names = self.print_names.unwrap_or(false) && !quiet;
422 let print_sizes = self.print_sizes.unwrap_or(false);
423
424 if print_names {
426 let mut artifacts: BTreeMap<_, Vec<_>> = BTreeMap::new();
427 for (name, (_, version)) in output.versioned_artifacts() {
428 artifacts.entry(version).or_default().push(name);
429 }
430
431 if shell::is_json() {
432 sh_println!("{}", serde_json::to_string(&artifacts).unwrap())?;
433 } else {
434 for (version, names) in artifacts {
435 sh_println!(
436 " compiler version: {}.{}.{}",
437 version.major,
438 version.minor,
439 version.patch
440 )?;
441 for name in names {
442 sh_println!(" - {name}")?;
443 }
444 }
445 }
446 }
447
448 if print_sizes {
449 if print_names && !shell::is_json() {
451 sh_println!()?;
452 }
453
454 let mut size_report =
455 SizeReport { contracts: BTreeMap::new(), limits: self.size_limits };
456
457 let mut artifacts: BTreeMap<String, Vec<_>> = BTreeMap::new();
458 for (id, artifact) in output.artifact_ids().filter(|(id, _)| {
459 !id.source.to_string_lossy().contains("/forge-std/src/")
461 }) {
462 artifacts.entry(id.name.clone()).or_default().push((id.source, artifact));
463 }
464
465 let abs_source = |path: &Path| -> PathBuf {
469 if path.is_absolute() { path.to_path_buf() } else { self.project_root.join(path) }
470 };
471 let source_paths = artifacts
472 .values()
473 .flatten()
474 .filter(|(_, artifact)| {
475 artifact.abi.as_ref().is_some_and(|abi| abi.functions().next().is_none())
476 })
477 .map(|(path, _)| abs_source(path))
478 .collect::<BTreeSet<_>>();
479 let libraries = collect_libraries(&source_paths);
480
481 for (name, artifact_list) in artifacts {
482 let kept = artifact_list
485 .iter()
486 .filter(|(path, artifact)| {
487 let is_library = libraries
488 .get(&abs_source(path))
489 .is_some_and(|libs| libs.contains(&name));
490 let has_no_abi_functions = artifact
491 .abi
492 .as_ref()
493 .is_some_and(|abi| abi.functions().next().is_none());
494 !(is_library && has_no_abi_functions)
495 })
496 .collect::<Vec<_>>();
497
498 for (path, artifact) in &kept {
499 let runtime_size = contract_size(*artifact, false).unwrap_or_default();
500 let init_size = contract_size(*artifact, true).unwrap_or_default();
501
502 let is_dev_contract = artifact.abi.as_ref().is_some_and(|abi| {
503 abi.functions().any(|f| {
504 f.test_function_kind().is_known()
505 || matches!(f.name.as_str(), "IS_TEST" | "IS_SCRIPT")
506 })
507 });
508
509 let unique_name = if kept.len() > 1 {
510 format!(
511 "{} ({})",
512 name,
513 path.strip_prefix(&self.project_root).unwrap_or(path).display()
514 )
515 } else {
516 name.clone()
517 };
518
519 size_report.contracts.insert(
520 unique_name,
521 ContractInfo { runtime_size, init_size, is_dev_contract },
522 );
523 }
524 }
525
526 if !quiet {
527 sh_println!("{size_report}")?;
528 }
529
530 let runtime_eip = match size_report.limits.runtime {
531 CONTRACT_RUNTIME_SIZE_LIMIT => "EIP-170: ",
532 AMSTERDAM_CONTRACT_RUNTIME_SIZE_LIMIT => "EIP-7954: ",
533 _ => "",
534 };
535 eyre::ensure!(
536 !size_report.exceeds_runtime_size_limit(),
537 "some contracts exceed the runtime size limit ({runtime_eip}{} bytes)",
538 size_report.limits.runtime
539 );
540 let initcode_eip = match size_report.limits.initcode {
542 CONTRACT_INITCODE_SIZE_LIMIT => "EIP-3860: ",
543 AMSTERDAM_CONTRACT_INITCODE_SIZE_LIMIT => "EIP-7954: ",
544 _ => "",
545 };
546 eyre::ensure!(
547 self.ignore_eip_3860 || !size_report.exceeds_initcode_size_limit(),
548 "some contracts exceed the initcode size limit ({initcode_eip}{} bytes)",
549 size_report.limits.initcode
550 );
551 }
552
553 Ok(())
554 }
555}
556
557const CONTRACT_RUNTIME_SIZE_LIMIT: usize = eip170::MAX_CODE_SIZE;
559
560const CONTRACT_INITCODE_SIZE_LIMIT: usize = eip3860::MAX_INITCODE_SIZE;
562
563const AMSTERDAM_CONTRACT_RUNTIME_SIZE_LIMIT: usize = 65_536;
565const AMSTERDAM_CONTRACT_INITCODE_SIZE_LIMIT: usize = 131_072;
566
567const CONTRACT_RUNTIME_SIZE_WARN_THRESHOLD: usize = 18_000;
568const CONTRACT_INITCODE_SIZE_WARN_THRESHOLD: usize = 36_000;
569
570#[derive(Clone, Copy, Debug, PartialEq, Eq)]
572pub struct ContractSizeLimits {
573 pub runtime: usize,
575 pub initcode: usize,
577}
578
579impl ContractSizeLimits {
580 pub const fn new(runtime: usize, initcode: usize) -> Self {
582 Self { runtime, initcode }
583 }
584
585 pub const fn with_runtime_limit(runtime: usize) -> Self {
587 Self { runtime, initcode: runtime.saturating_mul(2) }
588 }
589
590 pub const fn for_spec_id(spec_id: SpecId) -> Self {
592 if spec_id.is_enabled_in(SpecId::AMSTERDAM) {
593 Self::new(AMSTERDAM_CONTRACT_RUNTIME_SIZE_LIMIT, AMSTERDAM_CONTRACT_INITCODE_SIZE_LIMIT)
594 } else {
595 Self::new(CONTRACT_RUNTIME_SIZE_LIMIT, CONTRACT_INITCODE_SIZE_LIMIT)
596 }
597 }
598
599 const fn runtime_warning_threshold(self) -> usize {
600 scaled_threshold(
601 self.runtime,
602 CONTRACT_RUNTIME_SIZE_WARN_THRESHOLD,
603 CONTRACT_RUNTIME_SIZE_LIMIT,
604 )
605 }
606
607 const fn initcode_warning_threshold(self) -> usize {
608 scaled_threshold(
609 self.initcode,
610 CONTRACT_INITCODE_SIZE_WARN_THRESHOLD,
611 CONTRACT_INITCODE_SIZE_LIMIT,
612 )
613 }
614}
615
616impl Default for ContractSizeLimits {
617 fn default() -> Self {
618 Self::new(CONTRACT_RUNTIME_SIZE_LIMIT, CONTRACT_INITCODE_SIZE_LIMIT)
619 }
620}
621
622const fn scaled_threshold(limit: usize, threshold: usize, default_limit: usize) -> usize {
623 limit.saturating_mul(threshold) / default_limit
624}
625
626pub struct SizeReport {
628 pub contracts: BTreeMap<String, ContractInfo>,
630 pub limits: ContractSizeLimits,
632}
633
634impl SizeReport {
635 pub fn max_runtime_size(&self) -> usize {
637 self.contracts
638 .values()
639 .filter(|c| !c.is_dev_contract)
640 .map(|c| c.runtime_size)
641 .max()
642 .unwrap_or(0)
643 }
644
645 pub fn max_init_size(&self) -> usize {
647 self.contracts
648 .values()
649 .filter(|c| !c.is_dev_contract)
650 .map(|c| c.init_size)
651 .max()
652 .unwrap_or(0)
653 }
654
655 pub fn exceeds_runtime_size_limit(&self) -> bool {
657 self.max_runtime_size() > self.limits.runtime
658 }
659
660 pub fn exceeds_initcode_size_limit(&self) -> bool {
662 self.max_init_size() > self.limits.initcode
663 }
664}
665
666impl Display for SizeReport {
667 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
668 if shell::is_json() {
669 writeln!(f, "{}", self.format_json_output())?;
670 } else {
671 writeln!(f, "\n{}", self.format_table_output())?;
672 }
673 Ok(())
674 }
675}
676
677impl SizeReport {
678 fn format_json_output(&self) -> String {
679 let contracts = self
680 .contracts
681 .iter()
682 .filter(|(_, c)| !c.is_dev_contract && (c.runtime_size > 0 || c.init_size > 0))
683 .map(|(name, contract)| {
684 (
685 name.clone(),
686 serde_json::json!({
687 "runtime_size": contract.runtime_size,
688 "init_size": contract.init_size,
689 "runtime_margin": self.limits.runtime as isize - contract.runtime_size as isize,
690 "init_margin": self.limits.initcode as isize - contract.init_size as isize,
691 }),
692 )
693 })
694 .collect::<serde_json::Map<_, _>>();
695
696 serde_json::to_string(&contracts).unwrap()
697 }
698
699 fn format_table_output(&self) -> Table {
700 let mut table = Table::new();
701 if shell::is_markdown() {
702 table.load_style(ASCII_MARKDOWN);
703 } else {
704 table.load_style(ASCII_FULL.with_rounded_corners());
705 }
706
707 table.set_header(vec![
708 Cell::new("Contract"),
709 Cell::new("Runtime Size (B)"),
710 Cell::new("Initcode Size (B)"),
711 Cell::new("Runtime Margin (B)"),
712 Cell::new("Initcode Margin (B)"),
713 ]);
714
715 let contracts = self
717 .contracts
718 .iter()
719 .filter(|(_, c)| !c.is_dev_contract && (c.runtime_size > 0 || c.init_size > 0));
720 let runtime_warning_threshold = self.limits.runtime_warning_threshold();
721 let initcode_warning_threshold = self.limits.initcode_warning_threshold();
722 for (name, contract) in contracts {
723 let runtime_margin = self.limits.runtime as isize - contract.runtime_size as isize;
724 let init_margin = self.limits.initcode as isize - contract.init_size as isize;
725
726 let runtime_color = if contract.runtime_size < runtime_warning_threshold {
727 Color::Reset
728 } else if contract.runtime_size <= self.limits.runtime {
729 Color::Yellow
730 } else {
731 Color::Red
732 };
733
734 let init_color = if contract.init_size < initcode_warning_threshold {
735 Color::Reset
736 } else if contract.init_size <= self.limits.initcode {
737 Color::Yellow
738 } else {
739 Color::Red
740 };
741
742 let locale = &Locale::en;
743 table.add_row([
744 Cell::new(name),
745 Cell::new(contract.runtime_size.to_formatted_string(locale)).fg(runtime_color),
746 Cell::new(contract.init_size.to_formatted_string(locale)).fg(init_color),
747 Cell::new(runtime_margin.to_formatted_string(locale)).fg(runtime_color),
748 Cell::new(init_margin.to_formatted_string(locale)).fg(init_color),
749 ]);
750 }
751
752 table
753 }
754}
755
756fn collect_libraries(sources: &BTreeSet<PathBuf>) -> HashMap<PathBuf, HashSet<String>> {
760 let mut result: HashMap<PathBuf, HashSet<String>> = HashMap::new();
761 let sess = Session::builder().with_silent_emitter(None).build();
762 let _ = sess.enter(|| -> solar::interface::Result<()> {
763 for path in sources {
764 let arena = Arena::new();
765 let mut parser = match Parser::from_lazy_source_code(
766 &sess,
767 &arena,
768 FileName::from(path.clone()),
769 || std::fs::read_to_string(path),
770 ) {
771 Ok(parser) => parser,
772 Err(_) => continue,
773 };
774 let Ok(ast) = parser.parse_file() else { continue };
775 let libs = ast
776 .items
777 .iter()
778 .filter_map(|item| match &item.kind {
779 ItemKind::Contract(c) if c.kind == ContractKind::Library => {
780 Some(c.name.as_str().to_string())
781 }
782 _ => None,
783 })
784 .collect::<HashSet<_>>();
785 if !libs.is_empty() {
786 result.insert(path.clone(), libs);
787 }
788 }
789 Ok(())
790 });
791 result
792}
793
794fn contract_size<T: Artifact>(artifact: &T, initcode: bool) -> Option<usize> {
796 let bytecode = if initcode {
797 artifact.get_bytecode_object()?
798 } else {
799 artifact.get_deployed_bytecode_object()?
800 };
801
802 let size = match bytecode.as_ref() {
803 BytecodeObject::Bytecode(bytes) => bytes.len(),
804 BytecodeObject::Unlinked(unlinked) => {
805 let mut size = unlinked.len();
808 if unlinked.starts_with("0x") {
809 size -= 2;
810 }
811 size / 2
813 }
814 };
815
816 Some(size)
817}
818
819#[derive(Clone, Copy, Debug)]
821pub struct ContractInfo {
822 pub runtime_size: usize,
824 pub init_size: usize,
826 pub is_dev_contract: bool,
828}
829
830pub fn compile_target<C: Compiler<CompilerContract = Contract>>(
837 target_path: &Path,
838 project: &Project<C>,
839 quiet: bool,
840) -> Result<ProjectCompileOutput<C>>
841where
842 DynamicTestLinkingPreprocessor: Preprocessor<C>,
843{
844 ProjectCompiler::new().quiet(quiet).files([target_path.into()]).compile(project)
845}
846
847pub fn compile_abi_project<C: Compiler<CompilerContract = Contract>>(
849 project: &mut Project<C>,
850 mut compiler: ProjectCompiler,
851) -> Result<ProjectCompileOutput<C>>
852where
853 DynamicTestLinkingPreprocessor: Preprocessor<C>,
854{
855 project.update_output_selection(|selection| {
856 *selection = OutputSelection::common_output_selection(["abi".to_string()]);
858 });
859 compiler.abi_cache = (compiler.abi_cache || project.no_artifacts)
861 && project.compiler_severity_filter == Severity::Error;
862 compiler.compile(project)
863}
864
865pub fn compile_abi_project_cached<C: Compiler<CompilerContract = Contract>>(
869 project: &mut Project<C>,
870 mut compiler: ProjectCompiler,
871) -> Result<ProjectCompileOutput<C>>
872where
873 DynamicTestLinkingPreprocessor: Preprocessor<C>,
874{
875 if !project.cached
876 || project.build_info
877 || project.artifacts.additional_files != Default::default()
878 || project.compiler_severity_filter != Severity::Error
879 {
880 return compile_abi_project(project, compiler);
881 }
882 let mut cached_project = project.clone();
883 cached_project.no_artifacts = false;
884 compiler.abi_cache = true;
885 compiler.external_writes &= !project.no_artifacts;
886 compile_abi_project(&mut cached_project, compiler)
887}
888
889pub fn compile_target_abi(
891 project: &mut Project<MultiCompiler>,
892 target_path: &Path,
893 target_name: &str,
894) -> Result<JsonAbi> {
895 let target_path = canonicalize_path(target_path)?;
896 let output = compile_abi_project(
897 project,
898 ProjectCompiler::new().quiet(true).files([target_path.clone()]),
899 )?;
900
901 let artifact = output
902 .find(&target_path, target_name)
903 .ok_or_eyre("failed to find target artifact when compiling for abi")?;
904 artifact.abi.clone().ok_or_eyre("target artifact does not have an ABI")
905}
906
907pub fn etherscan_project(metadata: &Metadata, target_path: &Path) -> Result<Project> {
909 let target_path = canonicalize_path(target_path)?;
910 let sources_path = target_path.join(&metadata.contract_name);
911 metadata.source_tree().write_to(&target_path)?;
912
913 let mut settings = metadata.settings()?;
914
915 for remapping in &mut settings.remappings {
921 let new_path = sources_path.join(sanitize_relative_path(remapping.path.as_ref()));
922 remapping.path = new_path.display().to_string();
923 }
924
925 if !settings.remappings.iter().any(|remapping| remapping.name.starts_with("@openzeppelin/")) {
927 let oz = Remapping {
928 context: None,
929 name: "@openzeppelin/".into(),
930 path: sources_path.join("@openzeppelin").display().to_string(),
931 };
932 settings.remappings.push(oz);
933 }
934
935 let paths = ProjectPathsConfig::builder()
939 .sources(sources_path.clone())
940 .remappings(settings.remappings.clone())
941 .build_with_root(sources_path);
942
943 let v = metadata.compiler_version()?;
945 let solc = Solc::find_or_install(&v)?;
946
947 let compiler = MultiCompiler { solc: Some(SolcCompiler::Specific(solc)), vyper: None };
948
949 Ok(ProjectBuilder::<MultiCompiler>::default()
950 .settings(MultiCompilerSettings {
951 solc: SolcSettings {
952 settings: SolcConfig::builder().settings(settings).build(),
953 ..Default::default()
954 },
955 ..Default::default()
956 })
957 .paths(paths)
958 .ephemeral()
959 .no_artifacts()
960 .build(compiler)?)
961}
962
963fn sanitize_relative_path(path: &Path) -> PathBuf {
966 let mut normalized = PathBuf::new();
967 for component in path.components() {
968 match component {
969 Component::Normal(part) => normalized.push(part),
970 Component::ParentDir => {
971 normalized.pop();
972 }
973 _ => {}
974 }
975 }
976 normalized
977}
978
979pub fn add_storage_layout_output<C: Compiler<CompilerContract = Contract>>(
981 project: &mut Project<C>,
982) {
983 project.artifacts.additional_values.storage_layout = true;
984 project.update_output_selection(|selection| {
985 for contract_selection in selection.0.values_mut() {
986 for selection in contract_selection.values_mut() {
987 selection.push("storageLayout".to_string());
988 }
989 }
990 })
991}
992
993pub fn with_compilation_reporter<O>(
1000 quiet: bool,
1001 project_root: Option<PathBuf>,
1002 f: impl FnOnce() -> O,
1003) -> O {
1004 with_compilation_reporter_and_settings(quiet, project_root, false, f)
1005}
1006
1007fn with_compilation_reporter_and_settings<O>(
1008 quiet: bool,
1009 project_root: Option<PathBuf>,
1010 print_compiler_settings: bool,
1011 f: impl FnOnce() -> O,
1012) -> O {
1013 #[expect(clippy::collapsible_else_if)]
1014 let reporter = if quiet || shell::is_json() {
1015 Report::new(NoReporter::default())
1016 } else {
1017 if std::io::stderr().is_terminal() {
1018 Report::new(
1019 SpinnerReporter::spawn(project_root)
1020 .with_compiler_settings(print_compiler_settings),
1021 )
1022 } else {
1023 Report::new(
1024 BasicStdoutReporter::default().with_compiler_settings(print_compiler_settings),
1025 )
1026 }
1027 };
1028
1029 foundry_compilers::report::with_scoped(&reporter, f)
1030}
1031
1032#[derive(Clone, PartialEq, Eq)]
1039pub enum PathOrContractInfo {
1040 Path(PathBuf),
1042 ContractInfo(CompilerContractInfo),
1044}
1045
1046impl PathOrContractInfo {
1047 pub fn path(&self) -> Option<PathBuf> {
1049 match self {
1050 Self::Path(path) => Some(path.clone()),
1051 Self::ContractInfo(info) => info.path.as_ref().map(PathBuf::from),
1052 }
1053 }
1054
1055 pub fn name(&self) -> Option<&str> {
1057 match self {
1058 Self::Path(_) => None,
1059 Self::ContractInfo(info) => Some(&info.name),
1060 }
1061 }
1062}
1063
1064impl FromStr for PathOrContractInfo {
1065 type Err = eyre::Error;
1066
1067 fn from_str(s: &str) -> Result<Self> {
1068 if let Ok(contract) = CompilerContractInfo::from_str(s) {
1069 return Ok(Self::ContractInfo(contract));
1070 }
1071 let path = PathBuf::from(s);
1072 if path.extension().is_some_and(|ext| ext == "sol" || ext == "vy") {
1073 return Ok(Self::Path(path));
1074 }
1075 Err(eyre::eyre!("Invalid contract identifier, file is not *.sol or *.vy: {}", s))
1076 }
1077}
1078
1079impl std::fmt::Debug for PathOrContractInfo {
1080 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1081 match self {
1082 Self::Path(path) => write!(f, "Path({})", path.display()),
1083 Self::ContractInfo(info) => {
1084 write!(f, "ContractInfo({info})")
1085 }
1086 }
1087 }
1088}
1089
1090fn linked_libraries_error<C: Compiler<CompilerContract = Contract>>(
1094 output: &ProjectCompileOutput<C>,
1095 root: &Path,
1096) -> Option<String> {
1097 let violations = output
1098 .artifact_ids()
1099 .flat_map(|(id, artifact)| {
1100 let contract = id.with_stripped_file_prefixes(root).identifier();
1101 artifact.all_link_references().into_iter().flat_map(move |(file, libs)| {
1102 let contract = contract.clone();
1103 libs.into_keys().map(move |lib| format!("{contract} links {file}:{lib}"))
1104 })
1105 })
1106 .collect::<BTreeSet<_>>();
1107 (!violations.is_empty()).then(|| {
1108 format!(
1109 "Linked libraries are not allowed (`allow_linked_libraries = false`):\n {}\n\
1110 help: make the library functions `internal` to inline them, or set \
1111 `allow_linked_libraries = true`",
1112 violations.into_iter().collect::<Vec<_>>().join("\n ")
1113 )
1114 })
1115}
1116
1117fn push_linked_libraries_error<C: Compiler<CompilerContract = Contract>>(
1119 output: &mut ProjectCompileOutput<C>,
1120 message: String,
1121) {
1122 let error = SolcError {
1123 source_location: None,
1124 secondary_source_locations: Vec::new(),
1125 r#type: "LinkedLibraryError".to_string(),
1126 component: "foundry".to_string(),
1127 severity: Severity::Error,
1128 error_code: None,
1129 message,
1130 formatted_message: None,
1131 };
1132 let errors = &mut output.output_mut().errors as &mut dyn Any;
1134 if let Some(errors) = errors.downcast_mut::<Vec<SolcError>>() {
1135 errors.push(error);
1136 } else if let Some(errors) = errors.downcast_mut::<Vec<MultiCompilerError>>() {
1137 errors.push(MultiCompilerError::Solc(error));
1138 }
1139}
1140
1141fn dynamic_test_linking_failure<E: CompilationError>(errors: &[E]) -> Option<&'static str> {
1144 let mut reason = None;
1145 for error in errors.iter().filter(|error| error.is_error()) {
1146 let error = error as &dyn Any;
1149 let error = if let Some(error) = error.downcast_ref::<SolcError>() {
1150 error
1151 } else if let Some(MultiCompilerError::Solc(error)) =
1152 error.downcast_ref::<MultiCompilerError>()
1153 {
1154 error
1155 } else {
1156 return None;
1157 };
1158 reason = Some(match error.r#type.as_str() {
1159 "YulException" if error.message.contains("too deep in the stack") => "stack too deep",
1160 "CompilerError" if error.message.contains("Stack too deep") => "stack too deep",
1161 "InternalCompilerError"
1162 if error.message.contains("Tag too large for reserved space") =>
1163 {
1164 "Tag too large for reserved space"
1165 }
1166 _ => return None,
1167 });
1168 }
1169 reason
1170}
1171
1172#[cfg(test)]
1173mod tests {
1174 use super::*;
1175
1176 #[test]
1177 fn parse_contract_identifiers() {
1178 let t = ["src/Counter.sol", "src/Counter.sol:Counter", "Counter"];
1179
1180 let i1 = PathOrContractInfo::from_str(t[0]).unwrap();
1181 assert_eq!(i1, PathOrContractInfo::Path(PathBuf::from(t[0])));
1182
1183 let i2 = PathOrContractInfo::from_str(t[1]).unwrap();
1184 assert_eq!(
1185 i2,
1186 PathOrContractInfo::ContractInfo(CompilerContractInfo {
1187 path: Some("src/Counter.sol".to_string()),
1188 name: "Counter".to_string()
1189 })
1190 );
1191
1192 let i3 = PathOrContractInfo::from_str(t[2]).unwrap();
1193 assert_eq!(
1194 i3,
1195 PathOrContractInfo::ContractInfo(CompilerContractInfo {
1196 path: None,
1197 name: "Counter".to_string()
1198 })
1199 );
1200 }
1201
1202 #[test]
1203 fn size_report_uses_configured_limits() {
1204 let mut contracts = BTreeMap::new();
1205 contracts.insert(
1206 "LargeContract".to_string(),
1207 ContractInfo { runtime_size: 30_000, init_size: 60_000, is_dev_contract: false },
1208 );
1209
1210 let default_report =
1211 SizeReport { contracts: contracts.clone(), limits: ContractSizeLimits::default() };
1212 assert!(default_report.exceeds_runtime_size_limit());
1213 assert!(default_report.exceeds_initcode_size_limit());
1214
1215 let custom_report =
1216 SizeReport { contracts, limits: ContractSizeLimits::new(131_072, 262_144) };
1217 assert!(!custom_report.exceeds_runtime_size_limit());
1218 assert!(!custom_report.exceeds_initcode_size_limit());
1219 let output: serde_json::Value =
1220 serde_json::from_str(&custom_report.format_json_output()).unwrap();
1221 assert_eq!(
1222 output,
1223 serde_json::json!({
1224 "LargeContract": {
1225 "runtime_size": 30000,
1226 "init_size": 60000,
1227 "runtime_margin": 101072,
1228 "init_margin": 202144,
1229 }
1230 })
1231 );
1232 }
1233
1234 #[test]
1235 fn contract_size_limits_derive_initcode_limit_from_runtime_limit() {
1236 assert_eq!(
1237 ContractSizeLimits::with_runtime_limit(50_000),
1238 ContractSizeLimits::new(50_000, 100_000)
1239 );
1240 }
1241
1242 #[test]
1243 fn contract_size_limits_follow_evm_spec() {
1244 assert_eq!(ContractSizeLimits::for_spec_id(SpecId::OSAKA), ContractSizeLimits::default());
1245 assert_eq!(
1246 ContractSizeLimits::for_spec_id(SpecId::AMSTERDAM),
1247 ContractSizeLimits::new(65_536, 131_072)
1248 );
1249 }
1250
1251 #[test]
1252 fn sanitized_remapping_paths_stay_inside_the_root() {
1253 let root = Path::new("/tmp/sources");
1254 for path in ["../../../etc", "a/../../../etc", "/etc", "./a/../b"] {
1255 let joined = root.join(sanitize_relative_path(Path::new(path)));
1256 assert!(
1257 joined.starts_with(root) && !joined.components().any(|c| c == Component::ParentDir),
1258 "{path} escaped the root: {}",
1259 joined.display()
1260 );
1261 }
1262
1263 for (path, expected) in [("src/../lib/", "lib"), ("./a/../b", "b"), ("a/../../lib", "lib")]
1264 {
1265 assert_eq!(sanitize_relative_path(Path::new(path)), Path::new(expected));
1266 }
1267
1268 assert_eq!(
1270 sanitize_relative_path(Path::new("lib/openzeppelin")),
1271 Path::new("lib/openzeppelin")
1272 );
1273 }
1274}