1use crate::{
4 TestFunctionExt,
5 preprocessor::TestOptimizerPreprocessor,
6 reports::{ReportKind, report_kind},
7 shell,
8 term::SpinnerReporter,
9};
10use comfy_table::{Cell, Color, Table, modifiers::UTF8_ROUND_CORNERS};
11use eyre::Result;
12use foundry_block_explorers::contract::Metadata;
13use foundry_compilers::{
14 Artifact, Project, ProjectBuilder, ProjectCompileOutput, ProjectPathsConfig, SolcConfig,
15 artifacts::{BytecodeObject, Contract, Source, remappings::Remapping},
16 compilers::{
17 Compiler,
18 solc::{Solc, SolcCompiler},
19 },
20 info::ContractInfo as CompilerContractInfo,
21 project::Preprocessor,
22 report::{BasicStdoutReporter, NoReporter, Report},
23 solc::SolcSettings,
24};
25use num_format::{Locale, ToFormattedString};
26use std::{
27 collections::BTreeMap,
28 fmt::Display,
29 io::IsTerminal,
30 path::{Path, PathBuf},
31 str::FromStr,
32 time::Instant,
33};
34
35#[must_use = "ProjectCompiler does nothing unless you call a `compile*` method"]
40pub struct ProjectCompiler {
41 project_root: PathBuf,
43
44 verify: Option<bool>,
46
47 print_names: Option<bool>,
49
50 print_sizes: Option<bool>,
52
53 quiet: Option<bool>,
55
56 bail: Option<bool>,
58
59 ignore_eip_3860: bool,
61
62 files: Vec<PathBuf>,
64
65 dynamic_test_linking: bool,
67
68 runtime_size_limit: Option<usize>,
70
71 initcode_size_limit: Option<usize>,
73}
74
75impl Default for ProjectCompiler {
76 #[inline]
77 fn default() -> Self {
78 Self::new()
79 }
80}
81
82impl ProjectCompiler {
83 #[inline]
85 pub fn new() -> Self {
86 Self {
87 project_root: PathBuf::new(),
88 verify: None,
89 print_names: None,
90 print_sizes: None,
91 quiet: Some(crate::shell::is_quiet()),
92 bail: None,
93 ignore_eip_3860: false,
94 files: Vec::new(),
95 dynamic_test_linking: false,
96 runtime_size_limit: None,
97 initcode_size_limit: None,
98 }
99 }
100
101 #[inline]
103 pub fn verify(mut self, yes: bool) -> Self {
104 self.verify = Some(yes);
105 self
106 }
107
108 #[inline]
110 pub fn print_names(mut self, yes: bool) -> Self {
111 self.print_names = Some(yes);
112 self
113 }
114
115 #[inline]
117 pub fn print_sizes(mut self, yes: bool) -> Self {
118 self.print_sizes = Some(yes);
119 self
120 }
121
122 #[inline]
124 #[doc(alias = "silent")]
125 pub fn quiet(mut self, yes: bool) -> Self {
126 self.quiet = Some(yes);
127 self
128 }
129
130 #[inline]
132 pub fn bail(mut self, yes: bool) -> Self {
133 self.bail = Some(yes);
134 self
135 }
136
137 #[inline]
139 pub fn ignore_eip_3860(mut self, yes: bool) -> Self {
140 self.ignore_eip_3860 = yes;
141 self
142 }
143
144 #[inline]
146 pub fn files(mut self, files: impl IntoIterator<Item = PathBuf>) -> Self {
147 self.files.extend(files);
148 self
149 }
150
151 #[inline]
153 pub fn dynamic_test_linking(mut self, preprocess: bool) -> Self {
154 self.dynamic_test_linking = preprocess;
155 self
156 }
157
158 #[inline]
160 pub fn size_limits(mut self, runtime_size_limit: usize, initcode_size_limit: usize) -> Self {
161 self.runtime_size_limit = Some(runtime_size_limit);
162 self.initcode_size_limit = Some(initcode_size_limit);
163 self
164 }
165
166 pub fn compile<C: Compiler<CompilerContract = Contract>>(
168 mut self,
169 project: &Project<C>,
170 ) -> Result<ProjectCompileOutput<C>>
171 where
172 TestOptimizerPreprocessor: Preprocessor<C>,
173 {
174 self.project_root = project.root().to_path_buf();
175
176 if !project.paths.has_input_files() && self.files.is_empty() {
183 sh_println!("Nothing to compile")?;
184 std::process::exit(0);
185 }
186
187 let files = std::mem::take(&mut self.files);
189 let preprocess = self.dynamic_test_linking;
190 self.compile_with(|| {
191 let sources = if !files.is_empty() {
192 Source::read_all(files)?
193 } else {
194 project.paths.read_input_files()?
195 };
196
197 let mut compiler =
198 foundry_compilers::project::ProjectCompiler::with_sources(project, sources)?;
199 if preprocess {
200 compiler = compiler.with_preprocessor(TestOptimizerPreprocessor);
201 }
202 compiler.compile().map_err(Into::into)
203 })
204 }
205
206 #[instrument(target = "forge::compile", skip_all)]
217 fn compile_with<C: Compiler<CompilerContract = Contract>, F>(
218 self,
219 f: F,
220 ) -> Result<ProjectCompileOutput<C>>
221 where
222 F: FnOnce() -> Result<ProjectCompileOutput<C>>,
223 {
224 let quiet = self.quiet.unwrap_or(false);
225 let bail = self.bail.unwrap_or(true);
226
227 let output = with_compilation_reporter(quiet, || {
228 tracing::debug!("compiling project");
229
230 let timer = Instant::now();
231 let r = f();
232 let elapsed = timer.elapsed();
233
234 tracing::debug!("finished compiling in {:.3}s", elapsed.as_secs_f64());
235 r
236 })?;
237
238 if bail && output.has_compiler_errors() {
239 eyre::bail!("{output}")
240 }
241
242 if !quiet {
243 if !shell::is_json() {
244 if output.is_unchanged() {
245 sh_println!("No files changed, compilation skipped")?;
246 } else {
247 sh_println!("{output}")?;
249 }
250 }
251
252 self.handle_output(&output)?;
253 }
254
255 Ok(output)
256 }
257
258 fn handle_output<C: Compiler<CompilerContract = Contract>>(
260 &self,
261 output: &ProjectCompileOutput<C>,
262 ) -> Result<()> {
263 let print_names = self.print_names.unwrap_or(false);
264 let print_sizes = self.print_sizes.unwrap_or(false);
265
266 if print_names {
268 let mut artifacts: BTreeMap<_, Vec<_>> = BTreeMap::new();
269 for (name, (_, version)) in output.versioned_artifacts() {
270 artifacts.entry(version).or_default().push(name);
271 }
272
273 if shell::is_json() {
274 sh_println!("{}", serde_json::to_string(&artifacts).unwrap())?;
275 } else {
276 for (version, names) in artifacts {
277 sh_println!(
278 " compiler version: {}.{}.{}",
279 version.major,
280 version.minor,
281 version.patch
282 )?;
283 for name in names {
284 sh_println!(" - {name}")?;
285 }
286 }
287 }
288 }
289
290 if print_sizes {
291 if print_names && !shell::is_json() {
293 sh_println!()?;
294 }
295
296 let mut size_report = SizeReport::new(
297 report_kind(),
298 BTreeMap::new(),
299 self.runtime_size_limit.unwrap_or(CONTRACT_RUNTIME_SIZE_LIMIT),
300 self.initcode_size_limit.unwrap_or(CONTRACT_INITCODE_SIZE_LIMIT),
301 );
302
303 let mut artifacts: BTreeMap<String, Vec<_>> = BTreeMap::new();
304 for (id, artifact) in output.artifact_ids().filter(|(id, _)| {
305 !id.source.to_string_lossy().contains("/forge-std/src/")
307 }) {
308 artifacts.entry(id.name.clone()).or_default().push((id.source.clone(), artifact));
309 }
310
311 for (name, artifact_list) in artifacts {
312 for (path, artifact) in &artifact_list {
313 let runtime_size = contract_size(*artifact, false).unwrap_or_default();
314 let init_size = contract_size(*artifact, true).unwrap_or_default();
315
316 let is_dev_contract = artifact
317 .abi
318 .as_ref()
319 .map(|abi| {
320 abi.functions().any(|f| {
321 f.test_function_kind().is_known()
322 || matches!(f.name.as_str(), "IS_TEST" | "IS_SCRIPT")
323 })
324 })
325 .unwrap_or(false);
326
327 let unique_name = if artifact_list.len() > 1 {
328 format!(
329 "{} ({})",
330 name,
331 path.strip_prefix(&self.project_root).unwrap_or(path).display()
332 )
333 } else {
334 name.clone()
335 };
336
337 size_report.contracts.insert(
338 unique_name,
339 ContractInfo { runtime_size, init_size, is_dev_contract },
340 );
341 }
342 }
343
344 sh_println!("{size_report}")?;
345
346 eyre::ensure!(
347 !size_report.exceeds_runtime_size_limit(),
348 "some contracts exceed the runtime size limit \
349 (EIP-170: {CONTRACT_RUNTIME_SIZE_LIMIT} bytes)"
350 );
351 eyre::ensure!(
353 self.ignore_eip_3860 || !size_report.exceeds_initcode_size_limit(),
354 "some contracts exceed the initcode size limit \
355 (EIP-3860: {CONTRACT_INITCODE_SIZE_LIMIT} bytes)"
356 );
357 }
358
359 Ok(())
360 }
361}
362
363const CONTRACT_RUNTIME_SIZE_LIMIT: usize = 24576;
365
366const CONTRACT_INITCODE_SIZE_LIMIT: usize = 49152;
368
369const CONTRACT_SIZE_LIMIT_MARGIN: f64 = 0.73;
370pub struct SizeReport {
372 report_kind: ReportKind,
374 pub contracts: BTreeMap<String, ContractInfo>,
376 runtime_size_limit: usize,
378 initcode_size_limit: usize,
380}
381
382impl SizeReport {
383 pub fn new(
384 report_kind: ReportKind,
385 contracts: BTreeMap<String, ContractInfo>,
386 runtime_size_limit: usize,
387 initcode_size_limit: usize,
388 ) -> Self {
389 Self { report_kind, contracts, runtime_size_limit, initcode_size_limit }
390 }
391 pub fn max_runtime_size(&self) -> usize {
393 self.contracts
394 .values()
395 .filter(|c| !c.is_dev_contract)
396 .map(|c| c.runtime_size)
397 .max()
398 .unwrap_or(0)
399 }
400
401 pub fn max_init_size(&self) -> usize {
403 self.contracts
404 .values()
405 .filter(|c| !c.is_dev_contract)
406 .map(|c| c.init_size)
407 .max()
408 .unwrap_or(0)
409 }
410
411 pub fn exceeds_runtime_size_limit(&self) -> bool {
413 self.max_runtime_size() > self.runtime_size_limit
414 }
415
416 pub fn exceeds_initcode_size_limit(&self) -> bool {
418 self.max_init_size() > self.initcode_size_limit
419 }
420}
421
422impl Display for SizeReport {
423 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
424 match self.report_kind {
425 ReportKind::Text => {
426 writeln!(f, "\n{}", self.format_table_output())?;
427 }
428 ReportKind::JSON => {
429 writeln!(f, "{}", self.format_json_output())?;
430 }
431 }
432
433 Ok(())
434 }
435}
436
437impl SizeReport {
438 fn format_json_output(&self) -> String {
439 let contracts = self
440 .contracts
441 .iter()
442 .filter(|(_, c)| !c.is_dev_contract && (c.runtime_size > 0 || c.init_size > 0))
443 .map(|(name, contract)| {
444 (
445 name.clone(),
446 serde_json::json!({
447 "runtime_size": contract.runtime_size,
448 "init_size": contract.init_size,
449 "runtime_margin": self.runtime_size_limit as isize - contract.runtime_size as isize,
450 "init_margin": self.initcode_size_limit as isize - contract.init_size as isize,
451 }),
452 )
453 })
454 .collect::<serde_json::Map<_, _>>();
455
456 serde_json::to_string(&contracts).unwrap()
457 }
458
459 fn format_table_output(&self) -> Table {
460 let mut table = Table::new();
461 table.apply_modifier(UTF8_ROUND_CORNERS);
462
463 table.set_header(vec![
464 Cell::new("Contract"),
465 Cell::new("Runtime Size (B)"),
466 Cell::new("Initcode Size (B)"),
467 Cell::new("Runtime Margin (B)"),
468 Cell::new("Initcode Margin (B)"),
469 ]);
470
471 let contracts = self
473 .contracts
474 .iter()
475 .filter(|(_, c)| !c.is_dev_contract && (c.runtime_size > 0 || c.init_size > 0));
476 for (name, contract) in contracts {
477 let runtime_margin = self.runtime_size_limit as isize - contract.runtime_size as isize;
478 let init_margin = self.initcode_size_limit as isize - contract.init_size as isize;
479
480 let runtime_size_warning =
481 (CONTRACT_SIZE_LIMIT_MARGIN * self.runtime_size_limit as f64) as usize;
482 let initcode_size_warning =
483 (CONTRACT_SIZE_LIMIT_MARGIN * self.initcode_size_limit as f64) as usize;
484
485 let runtime_color = if contract.runtime_size < runtime_size_warning {
486 Color::Reset
487 } else if contract.runtime_size <= self.runtime_size_limit {
488 Color::Yellow
489 } else {
490 Color::Red
491 };
492
493 let init_color = if contract.init_size < initcode_size_warning {
494 Color::Reset
495 } else if contract.init_size <= self.initcode_size_limit {
496 Color::Yellow
497 } else {
498 Color::Red
499 };
500
501 let locale = &Locale::en;
502 table.add_row([
503 Cell::new(name),
504 Cell::new(contract.runtime_size.to_formatted_string(locale)).fg(runtime_color),
505 Cell::new(contract.init_size.to_formatted_string(locale)).fg(init_color),
506 Cell::new(runtime_margin.to_formatted_string(locale)).fg(runtime_color),
507 Cell::new(init_margin.to_formatted_string(locale)).fg(init_color),
508 ]);
509 }
510
511 table
512 }
513}
514
515fn contract_size<T: Artifact>(artifact: &T, initcode: bool) -> Option<usize> {
517 let bytecode = if initcode {
518 artifact.get_bytecode_object()?
519 } else {
520 artifact.get_deployed_bytecode_object()?
521 };
522
523 let size = match bytecode.as_ref() {
524 BytecodeObject::Bytecode(bytes) => bytes.len(),
525 BytecodeObject::Unlinked(unlinked) => {
526 let mut size = unlinked.len();
529 if unlinked.starts_with("0x") {
530 size -= 2;
531 }
532 size / 2
534 }
535 };
536
537 Some(size)
538}
539
540#[derive(Clone, Copy, Debug)]
542pub struct ContractInfo {
543 pub runtime_size: usize,
545 pub init_size: usize,
547 pub is_dev_contract: bool,
549}
550
551pub fn compile_target<C: Compiler<CompilerContract = Contract>>(
559 target_path: &Path,
560 project: &Project<C>,
561 quiet: bool,
562) -> Result<ProjectCompileOutput<C>>
563where
564 TestOptimizerPreprocessor: Preprocessor<C>,
565{
566 ProjectCompiler::new().quiet(quiet).files([target_path.into()]).compile(project)
567}
568
569pub fn etherscan_project(
571 metadata: &Metadata,
572 target_path: impl AsRef<Path>,
573) -> Result<Project<SolcCompiler>> {
574 let target_path = dunce::canonicalize(target_path.as_ref())?;
575 let sources_path = target_path.join(&metadata.contract_name);
576 metadata.source_tree().write_to(&target_path)?;
577
578 let mut settings = metadata.settings()?;
579
580 for remapping in &mut settings.remappings {
582 let new_path = sources_path.join(remapping.path.trim_start_matches('/'));
583 remapping.path = new_path.display().to_string();
584 }
585
586 if !settings.remappings.iter().any(|remapping| remapping.name.starts_with("@openzeppelin/")) {
588 let oz = Remapping {
589 context: None,
590 name: "@openzeppelin/".into(),
591 path: sources_path.join("@openzeppelin").display().to_string(),
592 };
593 settings.remappings.push(oz);
594 }
595
596 let paths = ProjectPathsConfig::builder()
600 .sources(sources_path.clone())
601 .remappings(settings.remappings.clone())
602 .build_with_root(sources_path);
603
604 let v = metadata.compiler_version()?;
605 let solc = Solc::find_or_install(&v)?;
606
607 let compiler = SolcCompiler::Specific(solc);
608
609 Ok(ProjectBuilder::<SolcCompiler>::default()
610 .settings(SolcSettings {
611 settings: SolcConfig::builder().settings(settings).build(),
612 ..Default::default()
613 })
614 .paths(paths)
615 .ephemeral()
616 .no_artifacts()
617 .build(compiler)?)
618}
619
620pub fn with_compilation_reporter<O>(quiet: bool, f: impl FnOnce() -> O) -> O {
622 #[expect(clippy::collapsible_else_if)]
623 let reporter = if quiet || shell::is_json() {
624 Report::new(NoReporter::default())
625 } else {
626 if std::io::stdout().is_terminal() {
627 Report::new(SpinnerReporter::spawn())
628 } else {
629 Report::new(BasicStdoutReporter::default())
630 }
631 };
632
633 foundry_compilers::report::with_scoped(&reporter, f)
634}
635
636#[derive(Clone, PartialEq, Eq)]
643pub enum PathOrContractInfo {
644 Path(PathBuf),
646 ContractInfo(CompilerContractInfo),
648}
649
650impl PathOrContractInfo {
651 pub fn path(&self) -> Option<PathBuf> {
653 match self {
654 Self::Path(path) => Some(path.to_path_buf()),
655 Self::ContractInfo(info) => info.path.as_ref().map(PathBuf::from),
656 }
657 }
658
659 pub fn name(&self) -> Option<&str> {
661 match self {
662 Self::Path(_) => None,
663 Self::ContractInfo(info) => Some(&info.name),
664 }
665 }
666}
667
668impl FromStr for PathOrContractInfo {
669 type Err = eyre::Error;
670
671 fn from_str(s: &str) -> Result<Self> {
672 if let Ok(contract) = CompilerContractInfo::from_str(s) {
673 return Ok(Self::ContractInfo(contract));
674 }
675 let path = PathBuf::from(s);
676 if path.extension().is_some_and(|ext| ext == "sol" || ext == "vy") {
677 return Ok(Self::Path(path));
678 }
679 Err(eyre::eyre!("Invalid contract identifier, file is not *.sol or *.vy: {}", s))
680 }
681}
682
683impl std::fmt::Debug for PathOrContractInfo {
684 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
685 match self {
686 Self::Path(path) => write!(f, "Path({})", path.display()),
687 Self::ContractInfo(info) => {
688 write!(f, "ContractInfo({info})")
689 }
690 }
691 }
692}
693
694#[cfg(test)]
695mod tests {
696 use super::*;
697
698 #[test]
699 fn parse_contract_identifiers() {
700 let t = ["src/Counter.sol", "src/Counter.sol:Counter", "Counter"];
701
702 let i1 = PathOrContractInfo::from_str(t[0]).unwrap();
703 assert_eq!(i1, PathOrContractInfo::Path(PathBuf::from(t[0])));
704
705 let i2 = PathOrContractInfo::from_str(t[1]).unwrap();
706 assert_eq!(
707 i2,
708 PathOrContractInfo::ContractInfo(CompilerContractInfo {
709 path: Some("src/Counter.sol".to_string()),
710 name: "Counter".to_string()
711 })
712 );
713
714 let i3 = PathOrContractInfo::from_str(t[2]).unwrap();
715 assert_eq!(
716 i3,
717 PathOrContractInfo::ContractInfo(CompilerContractInfo {
718 path: None,
719 name: "Counter".to_string()
720 })
721 );
722 }
723}