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, Severity, Source, output_selection::OutputSelection,
22 remappings::Remapping,
23 },
24 compilers::{
25 CompilationError, Compiler,
26 solc::{Solc, SolcCompiler},
27 },
28 info::ContractInfo as CompilerContractInfo,
29 multi::{MultiCompiler, 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 collections::{BTreeMap, BTreeSet, HashMap, HashSet},
44 fmt::Display,
45 io::IsTerminal,
46 path::{Component, Path, PathBuf},
47 str::FromStr,
48 sync::Arc,
49 time::Instant,
50};
51
52pub type Analysis = Arc<solar::sema::Compiler>;
54
55#[must_use = "ProjectCompiler does nothing unless you call a `compile*` method"]
60pub struct ProjectCompiler {
61 project_root: PathBuf,
63
64 print_names: Option<bool>,
66
67 print_sizes: Option<bool>,
69
70 quiet: Option<bool>,
72
73 print_compiler_settings: bool,
75
76 bail: Option<bool>,
78
79 ignore_eip_3860: bool,
81
82 size_limits: ContractSizeLimits,
84
85 files: Vec<PathBuf>,
87
88 source_order_fallback: Vec<PathBuf>,
90
91 selected_paths: Vec<PathBuf>,
93
94 dynamic_test_linking: bool,
96
97 abi_cache: bool,
99
100 external_compilers: Option<Config>,
102
103 external_writes: bool,
106}
107
108impl Default for ProjectCompiler {
109 #[inline]
110 fn default() -> Self {
111 Self::new()
112 }
113}
114
115impl ProjectCompiler {
116 #[inline]
118 pub fn new() -> Self {
119 Self {
120 project_root: PathBuf::new(),
121 print_names: None,
122 print_sizes: None,
123 quiet: Some(crate::shell::is_quiet()),
124 print_compiler_settings: false,
125 bail: None,
126 ignore_eip_3860: false,
127 size_limits: ContractSizeLimits::default(),
128 files: Vec::new(),
129 source_order_fallback: Vec::new(),
130 selected_paths: Vec::new(),
131 dynamic_test_linking: false,
132 abi_cache: false,
133 external_compilers: None,
134 external_writes: true,
135 }
136 }
137
138 #[inline]
140 pub const fn print_names(mut self, yes: bool) -> Self {
141 self.print_names = Some(yes);
142 self
143 }
144
145 #[inline]
147 pub const fn print_sizes(mut self, yes: bool) -> Self {
148 self.print_sizes = Some(yes);
149 self
150 }
151
152 #[inline]
154 #[doc(alias = "silent")]
155 pub const fn quiet(mut self, yes: bool) -> Self {
156 self.quiet = Some(yes);
157 self
158 }
159
160 #[inline]
162 pub const fn print_compiler_settings(mut self, yes: bool) -> Self {
163 self.print_compiler_settings = yes;
164 self
165 }
166
167 #[inline]
169 pub const fn bail(mut self, yes: bool) -> Self {
170 self.bail = Some(yes);
171 self
172 }
173
174 #[inline]
176 pub const fn ignore_eip_3860(mut self, yes: bool) -> Self {
177 self.ignore_eip_3860 = yes;
178 self
179 }
180
181 #[inline]
183 pub const fn size_limits(mut self, limits: ContractSizeLimits) -> Self {
184 self.size_limits = limits;
185 self
186 }
187
188 #[inline]
190 pub fn files(mut self, files: impl IntoIterator<Item = PathBuf>) -> Self {
191 self.files.extend(files);
192 self
193 }
194
195 pub fn selected_paths(mut self, paths: impl IntoIterator<Item = PathBuf>) -> Self {
197 self.selected_paths.extend(paths);
198 self
199 }
200
201 pub fn target_files(mut self, paths: impl IntoIterator<Item = PathBuf>) -> Self {
203 for path in paths {
204 if is_builtin_compiler_source(&path) {
205 self.files.push(path.clone());
206 }
207 self.selected_paths.push(path);
208 }
209 self
210 }
211
212 #[inline]
214 pub const fn dynamic_test_linking(mut self, preprocess: bool) -> Self {
215 self.dynamic_test_linking = preprocess;
216 self
217 }
218
219 pub fn external_compilers(mut self, config: &Config) -> Self {
221 self.external_compilers = Some(config.clone());
222 self
223 }
224
225 pub const fn external_artifacts(mut self, write: bool) -> Self {
227 self.external_writes = write;
228 self
229 }
230
231 pub const fn cache_abi(mut self) -> Self {
236 self.abi_cache = true;
237 self
238 }
239
240 pub fn source_order_fallback(mut self, files: impl IntoIterator<Item = PathBuf>) -> Self {
243 self.source_order_fallback.extend(files);
244 self
245 }
246
247 #[instrument(target = "forge::compile", skip_all)]
249 pub fn compile<C: Compiler<CompilerContract = Contract>>(
250 mut self,
251 project: &Project<C>,
252 ) -> Result<ProjectCompileOutput<C>>
253 where
254 DynamicTestLinkingPreprocessor: Preprocessor<C>,
255 {
256 self.project_root = project.root().to_path_buf();
257
258 if !project.paths.has_input_files()
265 && self.files.is_empty()
266 && self.selected_paths.is_empty()
267 && self
268 .external_compilers
269 .as_ref()
270 .is_none_or(|config| config.external_compilers.is_empty())
271 {
272 sh_println!("Nothing to compile")?;
273 std::process::exit(0);
274 }
275
276 let files = std::mem::take(&mut self.files);
278 let fallback_files = std::mem::take(&mut self.source_order_fallback);
279 let explicit_selection = !files.is_empty() || !self.selected_paths.is_empty();
280 let selected_paths = std::mem::take(&mut self.selected_paths);
281 let preprocess = self.dynamic_test_linking;
282 let abi_cache = self.abi_cache;
283 let external_compilers = self.external_compilers.take();
284 let external_writes = self.external_writes && !project.no_artifacts;
285 self.compile_with(|| {
286 let external = external_compilers
287 .as_ref()
288 .map(|config| {
289 ExternalCompilation::compile(config, &selected_paths, external_writes)
290 })
291 .transpose()?;
292 let sources = if explicit_selection {
293 Source::read_all(files)?
294 } else {
295 project.paths.read_input_files()?
296 };
297
298 let compile = |sources| {
299 let mut compiler =
300 foundry_compilers::project::ProjectCompiler::with_sources(project, sources)?;
301 if preprocess {
302 compiler = compiler.with_preprocessor(DynamicTestLinkingPreprocessor);
303 }
304 if abi_cache { compiler.compile_abi_cached() } else { compiler.compile() }
305 };
306 let mut output = compile(sources)?;
307 if !fallback_files.is_empty()
310 && output.output().errors.iter().any(|error| error.error_code() == Some(2449))
311 {
312 output = compile(Source::read_all(fallback_files)?)?;
313 }
314 if !output.has_compiler_errors()
315 && let Some(external) = external
316 {
317 external.merge(&mut output)?;
318 }
319 Ok(output)
320 })
321 }
322
323 fn compile_with<C: Compiler<CompilerContract = Contract>, F>(
325 self,
326 f: F,
327 ) -> Result<ProjectCompileOutput<C>>
328 where
329 F: FnOnce() -> Result<ProjectCompileOutput<C>>,
330 {
331 let quiet = self.quiet.unwrap_or(false);
332 let bail = self.bail.unwrap_or(true);
333
334 let output = with_compilation_reporter_and_settings(
335 quiet,
336 Some(self.project_root.clone()),
337 self.print_compiler_settings,
338 || {
339 tracing::debug!("compiling project");
340
341 let timer = Instant::now();
342 let r = f();
343 let elapsed = timer.elapsed();
344
345 tracing::debug!("finished compiling in {:.3}s", elapsed.as_secs_f64());
346 r
347 },
348 )?;
349
350 if bail && output.has_compiler_errors() {
351 eyre::bail!("{output}");
352 }
353
354 if !quiet && !shell::is_json() {
355 if output.is_unchanged() {
356 sh_println!("No files changed, compilation skipped")?;
357 } else {
358 sh_println!("{output}")?;
360 }
361 }
362
363 if !(shell::is_json() && output.has_compiler_errors()) {
365 self.handle_output(&output)?;
366 }
367
368 Ok(output)
369 }
370
371 fn handle_output<C: Compiler<CompilerContract = Contract>>(
373 &self,
374 output: &ProjectCompileOutput<C>,
375 ) -> Result<()> {
376 let quiet = self.quiet.unwrap_or(false);
377 let print_names = self.print_names.unwrap_or(false) && !quiet;
378 let print_sizes = self.print_sizes.unwrap_or(false);
379
380 if print_names {
382 let mut artifacts: BTreeMap<_, Vec<_>> = BTreeMap::new();
383 for (name, (_, version)) in output.versioned_artifacts() {
384 artifacts.entry(version).or_default().push(name);
385 }
386
387 if shell::is_json() {
388 sh_println!("{}", serde_json::to_string(&artifacts).unwrap())?;
389 } else {
390 for (version, names) in artifacts {
391 sh_println!(
392 " compiler version: {}.{}.{}",
393 version.major,
394 version.minor,
395 version.patch
396 )?;
397 for name in names {
398 sh_println!(" - {name}")?;
399 }
400 }
401 }
402 }
403
404 if print_sizes {
405 if print_names && !shell::is_json() {
407 sh_println!()?;
408 }
409
410 let mut size_report =
411 SizeReport { contracts: BTreeMap::new(), limits: self.size_limits };
412
413 let mut artifacts: BTreeMap<String, Vec<_>> = BTreeMap::new();
414 for (id, artifact) in output.artifact_ids().filter(|(id, _)| {
415 !id.source.to_string_lossy().contains("/forge-std/src/")
417 }) {
418 artifacts.entry(id.name.clone()).or_default().push((id.source, artifact));
419 }
420
421 let abs_source = |path: &Path| -> PathBuf {
425 if path.is_absolute() { path.to_path_buf() } else { self.project_root.join(path) }
426 };
427 let source_paths = artifacts
428 .values()
429 .flatten()
430 .filter(|(_, artifact)| {
431 artifact.abi.as_ref().is_some_and(|abi| abi.functions().next().is_none())
432 })
433 .map(|(path, _)| abs_source(path))
434 .collect::<BTreeSet<_>>();
435 let libraries = collect_libraries(&source_paths);
436
437 for (name, artifact_list) in artifacts {
438 let kept = artifact_list
441 .iter()
442 .filter(|(path, artifact)| {
443 let is_library = libraries
444 .get(&abs_source(path))
445 .is_some_and(|libs| libs.contains(&name));
446 let has_no_abi_functions = artifact
447 .abi
448 .as_ref()
449 .is_some_and(|abi| abi.functions().next().is_none());
450 !(is_library && has_no_abi_functions)
451 })
452 .collect::<Vec<_>>();
453
454 for (path, artifact) in &kept {
455 let runtime_size = contract_size(*artifact, false).unwrap_or_default();
456 let init_size = contract_size(*artifact, true).unwrap_or_default();
457
458 let is_dev_contract = artifact.abi.as_ref().is_some_and(|abi| {
459 abi.functions().any(|f| {
460 f.test_function_kind().is_known()
461 || matches!(f.name.as_str(), "IS_TEST" | "IS_SCRIPT")
462 })
463 });
464
465 let unique_name = if kept.len() > 1 {
466 format!(
467 "{} ({})",
468 name,
469 path.strip_prefix(&self.project_root).unwrap_or(path).display()
470 )
471 } else {
472 name.clone()
473 };
474
475 size_report.contracts.insert(
476 unique_name,
477 ContractInfo { runtime_size, init_size, is_dev_contract },
478 );
479 }
480 }
481
482 if !quiet {
483 sh_println!("{size_report}")?;
484 }
485
486 let runtime_eip = match size_report.limits.runtime {
487 CONTRACT_RUNTIME_SIZE_LIMIT => "EIP-170: ",
488 AMSTERDAM_CONTRACT_RUNTIME_SIZE_LIMIT => "EIP-7954: ",
489 _ => "",
490 };
491 eyre::ensure!(
492 !size_report.exceeds_runtime_size_limit(),
493 "some contracts exceed the runtime size limit ({runtime_eip}{} bytes)",
494 size_report.limits.runtime
495 );
496 let initcode_eip = match size_report.limits.initcode {
498 CONTRACT_INITCODE_SIZE_LIMIT => "EIP-3860: ",
499 AMSTERDAM_CONTRACT_INITCODE_SIZE_LIMIT => "EIP-7954: ",
500 _ => "",
501 };
502 eyre::ensure!(
503 self.ignore_eip_3860 || !size_report.exceeds_initcode_size_limit(),
504 "some contracts exceed the initcode size limit ({initcode_eip}{} bytes)",
505 size_report.limits.initcode
506 );
507 }
508
509 Ok(())
510 }
511}
512
513const CONTRACT_RUNTIME_SIZE_LIMIT: usize = eip170::MAX_CODE_SIZE;
515
516const CONTRACT_INITCODE_SIZE_LIMIT: usize = eip3860::MAX_INITCODE_SIZE;
518
519const AMSTERDAM_CONTRACT_RUNTIME_SIZE_LIMIT: usize = 65_536;
521const AMSTERDAM_CONTRACT_INITCODE_SIZE_LIMIT: usize = 131_072;
522
523const CONTRACT_RUNTIME_SIZE_WARN_THRESHOLD: usize = 18_000;
524const CONTRACT_INITCODE_SIZE_WARN_THRESHOLD: usize = 36_000;
525
526#[derive(Clone, Copy, Debug, PartialEq, Eq)]
528pub struct ContractSizeLimits {
529 pub runtime: usize,
531 pub initcode: usize,
533}
534
535impl ContractSizeLimits {
536 pub const fn new(runtime: usize, initcode: usize) -> Self {
538 Self { runtime, initcode }
539 }
540
541 pub const fn with_runtime_limit(runtime: usize) -> Self {
543 Self { runtime, initcode: runtime.saturating_mul(2) }
544 }
545
546 pub const fn for_spec_id(spec_id: SpecId) -> Self {
548 if spec_id.is_enabled_in(SpecId::AMSTERDAM) {
549 Self::new(AMSTERDAM_CONTRACT_RUNTIME_SIZE_LIMIT, AMSTERDAM_CONTRACT_INITCODE_SIZE_LIMIT)
550 } else {
551 Self::new(CONTRACT_RUNTIME_SIZE_LIMIT, CONTRACT_INITCODE_SIZE_LIMIT)
552 }
553 }
554
555 const fn runtime_warning_threshold(self) -> usize {
556 scaled_threshold(
557 self.runtime,
558 CONTRACT_RUNTIME_SIZE_WARN_THRESHOLD,
559 CONTRACT_RUNTIME_SIZE_LIMIT,
560 )
561 }
562
563 const fn initcode_warning_threshold(self) -> usize {
564 scaled_threshold(
565 self.initcode,
566 CONTRACT_INITCODE_SIZE_WARN_THRESHOLD,
567 CONTRACT_INITCODE_SIZE_LIMIT,
568 )
569 }
570}
571
572impl Default for ContractSizeLimits {
573 fn default() -> Self {
574 Self::new(CONTRACT_RUNTIME_SIZE_LIMIT, CONTRACT_INITCODE_SIZE_LIMIT)
575 }
576}
577
578const fn scaled_threshold(limit: usize, threshold: usize, default_limit: usize) -> usize {
579 limit.saturating_mul(threshold) / default_limit
580}
581
582pub struct SizeReport {
584 pub contracts: BTreeMap<String, ContractInfo>,
586 pub limits: ContractSizeLimits,
588}
589
590impl SizeReport {
591 pub fn max_runtime_size(&self) -> usize {
593 self.contracts
594 .values()
595 .filter(|c| !c.is_dev_contract)
596 .map(|c| c.runtime_size)
597 .max()
598 .unwrap_or(0)
599 }
600
601 pub fn max_init_size(&self) -> usize {
603 self.contracts
604 .values()
605 .filter(|c| !c.is_dev_contract)
606 .map(|c| c.init_size)
607 .max()
608 .unwrap_or(0)
609 }
610
611 pub fn exceeds_runtime_size_limit(&self) -> bool {
613 self.max_runtime_size() > self.limits.runtime
614 }
615
616 pub fn exceeds_initcode_size_limit(&self) -> bool {
618 self.max_init_size() > self.limits.initcode
619 }
620}
621
622impl Display for SizeReport {
623 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
624 if shell::is_json() {
625 writeln!(f, "{}", self.format_json_output())?;
626 } else {
627 writeln!(f, "\n{}", self.format_table_output())?;
628 }
629 Ok(())
630 }
631}
632
633impl SizeReport {
634 fn format_json_output(&self) -> String {
635 let contracts = self
636 .contracts
637 .iter()
638 .filter(|(_, c)| !c.is_dev_contract && (c.runtime_size > 0 || c.init_size > 0))
639 .map(|(name, contract)| {
640 (
641 name.clone(),
642 serde_json::json!({
643 "runtime_size": contract.runtime_size,
644 "init_size": contract.init_size,
645 "runtime_margin": self.limits.runtime as isize - contract.runtime_size as isize,
646 "init_margin": self.limits.initcode as isize - contract.init_size as isize,
647 }),
648 )
649 })
650 .collect::<serde_json::Map<_, _>>();
651
652 serde_json::to_string(&contracts).unwrap()
653 }
654
655 fn format_table_output(&self) -> Table {
656 let mut table = Table::new();
657 if shell::is_markdown() {
658 table.load_style(ASCII_MARKDOWN);
659 } else {
660 table.load_style(ASCII_FULL.with_rounded_corners());
661 }
662
663 table.set_header(vec![
664 Cell::new("Contract"),
665 Cell::new("Runtime Size (B)"),
666 Cell::new("Initcode Size (B)"),
667 Cell::new("Runtime Margin (B)"),
668 Cell::new("Initcode Margin (B)"),
669 ]);
670
671 let contracts = self
673 .contracts
674 .iter()
675 .filter(|(_, c)| !c.is_dev_contract && (c.runtime_size > 0 || c.init_size > 0));
676 let runtime_warning_threshold = self.limits.runtime_warning_threshold();
677 let initcode_warning_threshold = self.limits.initcode_warning_threshold();
678 for (name, contract) in contracts {
679 let runtime_margin = self.limits.runtime as isize - contract.runtime_size as isize;
680 let init_margin = self.limits.initcode as isize - contract.init_size as isize;
681
682 let runtime_color = if contract.runtime_size < runtime_warning_threshold {
683 Color::Reset
684 } else if contract.runtime_size <= self.limits.runtime {
685 Color::Yellow
686 } else {
687 Color::Red
688 };
689
690 let init_color = if contract.init_size < initcode_warning_threshold {
691 Color::Reset
692 } else if contract.init_size <= self.limits.initcode {
693 Color::Yellow
694 } else {
695 Color::Red
696 };
697
698 let locale = &Locale::en;
699 table.add_row([
700 Cell::new(name),
701 Cell::new(contract.runtime_size.to_formatted_string(locale)).fg(runtime_color),
702 Cell::new(contract.init_size.to_formatted_string(locale)).fg(init_color),
703 Cell::new(runtime_margin.to_formatted_string(locale)).fg(runtime_color),
704 Cell::new(init_margin.to_formatted_string(locale)).fg(init_color),
705 ]);
706 }
707
708 table
709 }
710}
711
712fn collect_libraries(sources: &BTreeSet<PathBuf>) -> HashMap<PathBuf, HashSet<String>> {
716 let mut result: HashMap<PathBuf, HashSet<String>> = HashMap::new();
717 let sess = Session::builder().with_silent_emitter(None).build();
718 let _ = sess.enter(|| -> solar::interface::Result<()> {
719 for path in sources {
720 let arena = Arena::new();
721 let mut parser = match Parser::from_lazy_source_code(
722 &sess,
723 &arena,
724 FileName::from(path.clone()),
725 || std::fs::read_to_string(path),
726 ) {
727 Ok(parser) => parser,
728 Err(_) => continue,
729 };
730 let Ok(ast) = parser.parse_file() else { continue };
731 let libs = ast
732 .items
733 .iter()
734 .filter_map(|item| match &item.kind {
735 ItemKind::Contract(c) if c.kind == ContractKind::Library => {
736 Some(c.name.as_str().to_string())
737 }
738 _ => None,
739 })
740 .collect::<HashSet<_>>();
741 if !libs.is_empty() {
742 result.insert(path.clone(), libs);
743 }
744 }
745 Ok(())
746 });
747 result
748}
749
750fn contract_size<T: Artifact>(artifact: &T, initcode: bool) -> Option<usize> {
752 let bytecode = if initcode {
753 artifact.get_bytecode_object()?
754 } else {
755 artifact.get_deployed_bytecode_object()?
756 };
757
758 let size = match bytecode.as_ref() {
759 BytecodeObject::Bytecode(bytes) => bytes.len(),
760 BytecodeObject::Unlinked(unlinked) => {
761 let mut size = unlinked.len();
764 if unlinked.starts_with("0x") {
765 size -= 2;
766 }
767 size / 2
769 }
770 };
771
772 Some(size)
773}
774
775#[derive(Clone, Copy, Debug)]
777pub struct ContractInfo {
778 pub runtime_size: usize,
780 pub init_size: usize,
782 pub is_dev_contract: bool,
784}
785
786pub fn compile_target<C: Compiler<CompilerContract = Contract>>(
793 target_path: &Path,
794 project: &Project<C>,
795 quiet: bool,
796) -> Result<ProjectCompileOutput<C>>
797where
798 DynamicTestLinkingPreprocessor: Preprocessor<C>,
799{
800 ProjectCompiler::new().quiet(quiet).files([target_path.into()]).compile(project)
801}
802
803pub fn compile_abi_project<C: Compiler<CompilerContract = Contract>>(
805 project: &mut Project<C>,
806 mut compiler: ProjectCompiler,
807) -> Result<ProjectCompileOutput<C>>
808where
809 DynamicTestLinkingPreprocessor: Preprocessor<C>,
810{
811 project.update_output_selection(|selection| {
812 *selection = OutputSelection::common_output_selection(["abi".to_string()]);
814 });
815 compiler.abi_cache = (compiler.abi_cache || project.no_artifacts)
817 && project.compiler_severity_filter == Severity::Error;
818 compiler.compile(project)
819}
820
821pub fn compile_abi_project_cached<C: Compiler<CompilerContract = Contract>>(
825 project: &mut Project<C>,
826 mut compiler: ProjectCompiler,
827) -> Result<ProjectCompileOutput<C>>
828where
829 DynamicTestLinkingPreprocessor: Preprocessor<C>,
830{
831 if !project.cached
832 || project.build_info
833 || project.artifacts.additional_files != Default::default()
834 || project.compiler_severity_filter != Severity::Error
835 {
836 return compile_abi_project(project, compiler);
837 }
838 let mut cached_project = project.clone();
839 cached_project.no_artifacts = false;
840 compiler.abi_cache = true;
841 compiler.external_writes &= !project.no_artifacts;
842 compile_abi_project(&mut cached_project, compiler)
843}
844
845pub fn compile_target_abi(
847 project: &mut Project<MultiCompiler>,
848 target_path: &Path,
849 target_name: &str,
850) -> Result<JsonAbi> {
851 let target_path = canonicalize_path(target_path)?;
852 let output = compile_abi_project(
853 project,
854 ProjectCompiler::new().quiet(true).files([target_path.clone()]),
855 )?;
856
857 let artifact = output
858 .find(&target_path, target_name)
859 .ok_or_eyre("failed to find target artifact when compiling for abi")?;
860 artifact.abi.clone().ok_or_eyre("target artifact does not have an ABI")
861}
862
863pub fn etherscan_project(metadata: &Metadata, target_path: &Path) -> Result<Project> {
865 let target_path = canonicalize_path(target_path)?;
866 let sources_path = target_path.join(&metadata.contract_name);
867 metadata.source_tree().write_to(&target_path)?;
868
869 let mut settings = metadata.settings()?;
870
871 for remapping in &mut settings.remappings {
877 let new_path = sources_path.join(sanitize_relative_path(remapping.path.as_ref()));
878 remapping.path = new_path.display().to_string();
879 }
880
881 if !settings.remappings.iter().any(|remapping| remapping.name.starts_with("@openzeppelin/")) {
883 let oz = Remapping {
884 context: None,
885 name: "@openzeppelin/".into(),
886 path: sources_path.join("@openzeppelin").display().to_string(),
887 };
888 settings.remappings.push(oz);
889 }
890
891 let paths = ProjectPathsConfig::builder()
895 .sources(sources_path.clone())
896 .remappings(settings.remappings.clone())
897 .build_with_root(sources_path);
898
899 let v = metadata.compiler_version()?;
901 let solc = Solc::find_or_install(&v)?;
902
903 let compiler = MultiCompiler { solc: Some(SolcCompiler::Specific(solc)), vyper: None };
904
905 Ok(ProjectBuilder::<MultiCompiler>::default()
906 .settings(MultiCompilerSettings {
907 solc: SolcSettings {
908 settings: SolcConfig::builder().settings(settings).build(),
909 ..Default::default()
910 },
911 ..Default::default()
912 })
913 .paths(paths)
914 .ephemeral()
915 .no_artifacts()
916 .build(compiler)?)
917}
918
919fn sanitize_relative_path(path: &Path) -> PathBuf {
922 let mut normalized = PathBuf::new();
923 for component in path.components() {
924 match component {
925 Component::Normal(part) => normalized.push(part),
926 Component::ParentDir => {
927 normalized.pop();
928 }
929 _ => {}
930 }
931 }
932 normalized
933}
934
935pub fn add_storage_layout_output<C: Compiler<CompilerContract = Contract>>(
937 project: &mut Project<C>,
938) {
939 project.artifacts.additional_values.storage_layout = true;
940 project.update_output_selection(|selection| {
941 for contract_selection in selection.0.values_mut() {
942 for selection in contract_selection.values_mut() {
943 selection.push("storageLayout".to_string());
944 }
945 }
946 })
947}
948
949pub fn with_compilation_reporter<O>(
956 quiet: bool,
957 project_root: Option<PathBuf>,
958 f: impl FnOnce() -> O,
959) -> O {
960 with_compilation_reporter_and_settings(quiet, project_root, false, f)
961}
962
963fn with_compilation_reporter_and_settings<O>(
964 quiet: bool,
965 project_root: Option<PathBuf>,
966 print_compiler_settings: bool,
967 f: impl FnOnce() -> O,
968) -> O {
969 #[expect(clippy::collapsible_else_if)]
970 let reporter = if quiet || shell::is_json() {
971 Report::new(NoReporter::default())
972 } else {
973 if std::io::stderr().is_terminal() {
974 Report::new(
975 SpinnerReporter::spawn(project_root)
976 .with_compiler_settings(print_compiler_settings),
977 )
978 } else {
979 Report::new(
980 BasicStdoutReporter::default().with_compiler_settings(print_compiler_settings),
981 )
982 }
983 };
984
985 foundry_compilers::report::with_scoped(&reporter, f)
986}
987
988#[derive(Clone, PartialEq, Eq)]
995pub enum PathOrContractInfo {
996 Path(PathBuf),
998 ContractInfo(CompilerContractInfo),
1000}
1001
1002impl PathOrContractInfo {
1003 pub fn path(&self) -> Option<PathBuf> {
1005 match self {
1006 Self::Path(path) => Some(path.clone()),
1007 Self::ContractInfo(info) => info.path.as_ref().map(PathBuf::from),
1008 }
1009 }
1010
1011 pub fn name(&self) -> Option<&str> {
1013 match self {
1014 Self::Path(_) => None,
1015 Self::ContractInfo(info) => Some(&info.name),
1016 }
1017 }
1018}
1019
1020impl FromStr for PathOrContractInfo {
1021 type Err = eyre::Error;
1022
1023 fn from_str(s: &str) -> Result<Self> {
1024 if let Ok(contract) = CompilerContractInfo::from_str(s) {
1025 return Ok(Self::ContractInfo(contract));
1026 }
1027 let path = PathBuf::from(s);
1028 if path.extension().is_some_and(|ext| ext == "sol" || ext == "vy") {
1029 return Ok(Self::Path(path));
1030 }
1031 Err(eyre::eyre!("Invalid contract identifier, file is not *.sol or *.vy: {}", s))
1032 }
1033}
1034
1035impl std::fmt::Debug for PathOrContractInfo {
1036 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1037 match self {
1038 Self::Path(path) => write!(f, "Path({})", path.display()),
1039 Self::ContractInfo(info) => {
1040 write!(f, "ContractInfo({info})")
1041 }
1042 }
1043 }
1044}
1045
1046#[cfg(test)]
1047mod tests {
1048 use super::*;
1049
1050 #[test]
1051 fn parse_contract_identifiers() {
1052 let t = ["src/Counter.sol", "src/Counter.sol:Counter", "Counter"];
1053
1054 let i1 = PathOrContractInfo::from_str(t[0]).unwrap();
1055 assert_eq!(i1, PathOrContractInfo::Path(PathBuf::from(t[0])));
1056
1057 let i2 = PathOrContractInfo::from_str(t[1]).unwrap();
1058 assert_eq!(
1059 i2,
1060 PathOrContractInfo::ContractInfo(CompilerContractInfo {
1061 path: Some("src/Counter.sol".to_string()),
1062 name: "Counter".to_string()
1063 })
1064 );
1065
1066 let i3 = PathOrContractInfo::from_str(t[2]).unwrap();
1067 assert_eq!(
1068 i3,
1069 PathOrContractInfo::ContractInfo(CompilerContractInfo {
1070 path: None,
1071 name: "Counter".to_string()
1072 })
1073 );
1074 }
1075
1076 #[test]
1077 fn size_report_uses_configured_limits() {
1078 let mut contracts = BTreeMap::new();
1079 contracts.insert(
1080 "LargeContract".to_string(),
1081 ContractInfo { runtime_size: 30_000, init_size: 60_000, is_dev_contract: false },
1082 );
1083
1084 let default_report =
1085 SizeReport { contracts: contracts.clone(), limits: ContractSizeLimits::default() };
1086 assert!(default_report.exceeds_runtime_size_limit());
1087 assert!(default_report.exceeds_initcode_size_limit());
1088
1089 let custom_report =
1090 SizeReport { contracts, limits: ContractSizeLimits::new(131_072, 262_144) };
1091 assert!(!custom_report.exceeds_runtime_size_limit());
1092 assert!(!custom_report.exceeds_initcode_size_limit());
1093 let output: serde_json::Value =
1094 serde_json::from_str(&custom_report.format_json_output()).unwrap();
1095 assert_eq!(
1096 output,
1097 serde_json::json!({
1098 "LargeContract": {
1099 "runtime_size": 30000,
1100 "init_size": 60000,
1101 "runtime_margin": 101072,
1102 "init_margin": 202144,
1103 }
1104 })
1105 );
1106 }
1107
1108 #[test]
1109 fn contract_size_limits_derive_initcode_limit_from_runtime_limit() {
1110 assert_eq!(
1111 ContractSizeLimits::with_runtime_limit(50_000),
1112 ContractSizeLimits::new(50_000, 100_000)
1113 );
1114 }
1115
1116 #[test]
1117 fn contract_size_limits_follow_evm_spec() {
1118 assert_eq!(ContractSizeLimits::for_spec_id(SpecId::OSAKA), ContractSizeLimits::default());
1119 assert_eq!(
1120 ContractSizeLimits::for_spec_id(SpecId::AMSTERDAM),
1121 ContractSizeLimits::new(65_536, 131_072)
1122 );
1123 }
1124
1125 #[test]
1126 fn sanitized_remapping_paths_stay_inside_the_root() {
1127 let root = Path::new("/tmp/sources");
1128 for path in ["../../../etc", "a/../../../etc", "/etc", "./a/../b"] {
1129 let joined = root.join(sanitize_relative_path(Path::new(path)));
1130 assert!(
1131 joined.starts_with(root) && !joined.components().any(|c| c == Component::ParentDir),
1132 "{path} escaped the root: {}",
1133 joined.display()
1134 );
1135 }
1136
1137 for (path, expected) in [("src/../lib/", "lib"), ("./a/../b", "b"), ("a/../../lib", "lib")]
1138 {
1139 assert_eq!(sanitize_relative_path(Path::new(path)), Path::new(expected));
1140 }
1141
1142 assert_eq!(
1144 sanitize_relative_path(Path::new("lib/openzeppelin")),
1145 Path::new("lib/openzeppelin")
1146 );
1147 }
1148}