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
69impl Default for ProjectCompiler {
70 #[inline]
71 fn default() -> Self {
72 Self::new()
73 }
74}
75
76impl ProjectCompiler {
77 #[inline]
79 pub fn new() -> Self {
80 Self {
81 project_root: PathBuf::new(),
82 verify: None,
83 print_names: None,
84 print_sizes: None,
85 quiet: Some(crate::shell::is_quiet()),
86 bail: None,
87 ignore_eip_3860: false,
88 files: Vec::new(),
89 dynamic_test_linking: false,
90 }
91 }
92
93 #[inline]
95 pub fn verify(mut self, yes: bool) -> Self {
96 self.verify = Some(yes);
97 self
98 }
99
100 #[inline]
102 pub fn print_names(mut self, yes: bool) -> Self {
103 self.print_names = Some(yes);
104 self
105 }
106
107 #[inline]
109 pub fn print_sizes(mut self, yes: bool) -> Self {
110 self.print_sizes = Some(yes);
111 self
112 }
113
114 #[inline]
116 #[doc(alias = "silent")]
117 pub fn quiet(mut self, yes: bool) -> Self {
118 self.quiet = Some(yes);
119 self
120 }
121
122 #[inline]
124 pub fn bail(mut self, yes: bool) -> Self {
125 self.bail = Some(yes);
126 self
127 }
128
129 #[inline]
131 pub fn ignore_eip_3860(mut self, yes: bool) -> Self {
132 self.ignore_eip_3860 = yes;
133 self
134 }
135
136 #[inline]
138 pub fn files(mut self, files: impl IntoIterator<Item = PathBuf>) -> Self {
139 self.files.extend(files);
140 self
141 }
142
143 #[inline]
145 pub fn dynamic_test_linking(mut self, preprocess: bool) -> Self {
146 self.dynamic_test_linking = preprocess;
147 self
148 }
149 pub fn compile<C: Compiler<CompilerContract = Contract>>(
151 mut self,
152 project: &Project<C>,
153 ) -> Result<ProjectCompileOutput<C>>
154 where
155 TestOptimizerPreprocessor: Preprocessor<C>,
156 {
157 self.project_root = project.root().to_path_buf();
158
159 if !project.paths.has_input_files() && self.files.is_empty() {
166 sh_println!("Nothing to compile")?;
167 std::process::exit(0);
168 }
169
170 let files = std::mem::take(&mut self.files);
172 let preprocess = self.dynamic_test_linking;
173 self.compile_with(|| {
174 let sources = if !files.is_empty() {
175 Source::read_all(files)?
176 } else {
177 project.paths.read_input_files()?
178 };
179
180 let mut compiler =
181 foundry_compilers::project::ProjectCompiler::with_sources(project, sources)?;
182 if preprocess {
183 compiler = compiler.with_preprocessor(TestOptimizerPreprocessor);
184 }
185 compiler.compile().map_err(Into::into)
186 })
187 }
188
189 #[instrument(target = "forge::compile", skip_all)]
200 fn compile_with<C: Compiler<CompilerContract = Contract>, F>(
201 self,
202 f: F,
203 ) -> Result<ProjectCompileOutput<C>>
204 where
205 F: FnOnce() -> Result<ProjectCompileOutput<C>>,
206 {
207 let quiet = self.quiet.unwrap_or(false);
208 let bail = self.bail.unwrap_or(true);
209
210 let output = with_compilation_reporter(quiet, || {
211 tracing::debug!("compiling project");
212
213 let timer = Instant::now();
214 let r = f();
215 let elapsed = timer.elapsed();
216
217 tracing::debug!("finished compiling in {:.3}s", elapsed.as_secs_f64());
218 r
219 })?;
220
221 if bail && output.has_compiler_errors() {
222 eyre::bail!("{output}")
223 }
224
225 if !quiet {
226 if !shell::is_json() {
227 if output.is_unchanged() {
228 sh_println!("No files changed, compilation skipped")?;
229 } else {
230 sh_println!("{output}")?;
232 }
233 }
234
235 self.handle_output(&output)?;
236 }
237
238 Ok(output)
239 }
240
241 fn handle_output<C: Compiler<CompilerContract = Contract>>(
243 &self,
244 output: &ProjectCompileOutput<C>,
245 ) -> Result<()> {
246 let print_names = self.print_names.unwrap_or(false);
247 let print_sizes = self.print_sizes.unwrap_or(false);
248
249 if print_names {
251 let mut artifacts: BTreeMap<_, Vec<_>> = BTreeMap::new();
252 for (name, (_, version)) in output.versioned_artifacts() {
253 artifacts.entry(version).or_default().push(name);
254 }
255
256 if shell::is_json() {
257 sh_println!("{}", serde_json::to_string(&artifacts).unwrap())?;
258 } else {
259 for (version, names) in artifacts {
260 sh_println!(
261 " compiler version: {}.{}.{}",
262 version.major,
263 version.minor,
264 version.patch
265 )?;
266 for name in names {
267 sh_println!(" - {name}")?;
268 }
269 }
270 }
271 }
272
273 if print_sizes {
274 if print_names && !shell::is_json() {
276 sh_println!()?;
277 }
278
279 let mut size_report =
280 SizeReport { report_kind: report_kind(), contracts: BTreeMap::new() };
281
282 let mut artifacts: BTreeMap<String, Vec<_>> = BTreeMap::new();
283 for (id, artifact) in output.artifact_ids().filter(|(id, _)| {
284 !id.source.to_string_lossy().contains("/forge-std/src/")
286 }) {
287 artifacts.entry(id.name.clone()).or_default().push((id.source.clone(), artifact));
288 }
289
290 for (name, artifact_list) in artifacts {
291 for (path, artifact) in &artifact_list {
292 let runtime_size = contract_size(*artifact, false).unwrap_or_default();
293 let init_size = contract_size(*artifact, true).unwrap_or_default();
294
295 let is_dev_contract = artifact
296 .abi
297 .as_ref()
298 .map(|abi| {
299 abi.functions().any(|f| {
300 f.test_function_kind().is_known()
301 || matches!(f.name.as_str(), "IS_TEST" | "IS_SCRIPT")
302 })
303 })
304 .unwrap_or(false);
305
306 let unique_name = if artifact_list.len() > 1 {
307 format!(
308 "{} ({})",
309 name,
310 path.strip_prefix(&self.project_root).unwrap_or(path).display()
311 )
312 } else {
313 name.clone()
314 };
315
316 size_report.contracts.insert(
317 unique_name,
318 ContractInfo { runtime_size, init_size, is_dev_contract },
319 );
320 }
321 }
322
323 sh_println!("{size_report}")?;
324
325 eyre::ensure!(
326 !size_report.exceeds_runtime_size_limit(),
327 "some contracts exceed the runtime size limit \
328 (EIP-170: {CONTRACT_RUNTIME_SIZE_LIMIT} bytes)"
329 );
330 eyre::ensure!(
332 self.ignore_eip_3860 || !size_report.exceeds_initcode_size_limit(),
333 "some contracts exceed the initcode size limit \
334 (EIP-3860: {CONTRACT_INITCODE_SIZE_LIMIT} bytes)"
335 );
336 }
337
338 Ok(())
339 }
340}
341
342const CONTRACT_RUNTIME_SIZE_LIMIT: usize = 24576;
344
345const CONTRACT_INITCODE_SIZE_LIMIT: usize = 49152;
347
348pub struct SizeReport {
350 report_kind: ReportKind,
352 pub contracts: BTreeMap<String, ContractInfo>,
354}
355
356impl SizeReport {
357 pub fn max_runtime_size(&self) -> usize {
359 self.contracts
360 .values()
361 .filter(|c| !c.is_dev_contract)
362 .map(|c| c.runtime_size)
363 .max()
364 .unwrap_or(0)
365 }
366
367 pub fn max_init_size(&self) -> usize {
369 self.contracts
370 .values()
371 .filter(|c| !c.is_dev_contract)
372 .map(|c| c.init_size)
373 .max()
374 .unwrap_or(0)
375 }
376
377 pub fn exceeds_runtime_size_limit(&self) -> bool {
379 self.max_runtime_size() > CONTRACT_RUNTIME_SIZE_LIMIT
380 }
381
382 pub fn exceeds_initcode_size_limit(&self) -> bool {
384 self.max_init_size() > CONTRACT_INITCODE_SIZE_LIMIT
385 }
386}
387
388impl Display for SizeReport {
389 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
390 match self.report_kind {
391 ReportKind::Text => {
392 writeln!(f, "\n{}", self.format_table_output())?;
393 }
394 ReportKind::JSON => {
395 writeln!(f, "{}", self.format_json_output())?;
396 }
397 }
398
399 Ok(())
400 }
401}
402
403impl SizeReport {
404 fn format_json_output(&self) -> String {
405 let contracts = self
406 .contracts
407 .iter()
408 .filter(|(_, c)| !c.is_dev_contract && (c.runtime_size > 0 || c.init_size > 0))
409 .map(|(name, contract)| {
410 (
411 name.clone(),
412 serde_json::json!({
413 "runtime_size": contract.runtime_size,
414 "init_size": contract.init_size,
415 "runtime_margin": CONTRACT_RUNTIME_SIZE_LIMIT as isize - contract.runtime_size as isize,
416 "init_margin": CONTRACT_INITCODE_SIZE_LIMIT as isize - contract.init_size as isize,
417 }),
418 )
419 })
420 .collect::<serde_json::Map<_, _>>();
421
422 serde_json::to_string(&contracts).unwrap()
423 }
424
425 fn format_table_output(&self) -> Table {
426 let mut table = Table::new();
427 table.apply_modifier(UTF8_ROUND_CORNERS);
428
429 table.set_header(vec![
430 Cell::new("Contract"),
431 Cell::new("Runtime Size (B)"),
432 Cell::new("Initcode Size (B)"),
433 Cell::new("Runtime Margin (B)"),
434 Cell::new("Initcode Margin (B)"),
435 ]);
436
437 let contracts = self
439 .contracts
440 .iter()
441 .filter(|(_, c)| !c.is_dev_contract && (c.runtime_size > 0 || c.init_size > 0));
442 for (name, contract) in contracts {
443 let runtime_margin =
444 CONTRACT_RUNTIME_SIZE_LIMIT as isize - contract.runtime_size as isize;
445 let init_margin = CONTRACT_INITCODE_SIZE_LIMIT as isize - contract.init_size as isize;
446
447 let runtime_color = match contract.runtime_size {
448 ..18_000 => Color::Reset,
449 18_000..=CONTRACT_RUNTIME_SIZE_LIMIT => Color::Yellow,
450 _ => Color::Red,
451 };
452
453 let init_color = match contract.init_size {
454 ..36_000 => Color::Reset,
455 36_000..=CONTRACT_INITCODE_SIZE_LIMIT => Color::Yellow,
456 _ => Color::Red,
457 };
458
459 let locale = &Locale::en;
460 table.add_row([
461 Cell::new(name),
462 Cell::new(contract.runtime_size.to_formatted_string(locale)).fg(runtime_color),
463 Cell::new(contract.init_size.to_formatted_string(locale)).fg(init_color),
464 Cell::new(runtime_margin.to_formatted_string(locale)).fg(runtime_color),
465 Cell::new(init_margin.to_formatted_string(locale)).fg(init_color),
466 ]);
467 }
468
469 table
470 }
471}
472
473fn contract_size<T: Artifact>(artifact: &T, initcode: bool) -> Option<usize> {
475 let bytecode = if initcode {
476 artifact.get_bytecode_object()?
477 } else {
478 artifact.get_deployed_bytecode_object()?
479 };
480
481 let size = match bytecode.as_ref() {
482 BytecodeObject::Bytecode(bytes) => bytes.len(),
483 BytecodeObject::Unlinked(unlinked) => {
484 let mut size = unlinked.len();
487 if unlinked.starts_with("0x") {
488 size -= 2;
489 }
490 size / 2
492 }
493 };
494
495 Some(size)
496}
497
498#[derive(Clone, Copy, Debug)]
500pub struct ContractInfo {
501 pub runtime_size: usize,
503 pub init_size: usize,
505 pub is_dev_contract: bool,
507}
508
509pub fn compile_target<C: Compiler<CompilerContract = Contract>>(
517 target_path: &Path,
518 project: &Project<C>,
519 quiet: bool,
520) -> Result<ProjectCompileOutput<C>>
521where
522 TestOptimizerPreprocessor: Preprocessor<C>,
523{
524 ProjectCompiler::new().quiet(quiet).files([target_path.into()]).compile(project)
525}
526
527pub fn etherscan_project(
529 metadata: &Metadata,
530 target_path: impl AsRef<Path>,
531) -> Result<Project<SolcCompiler>> {
532 let target_path = dunce::canonicalize(target_path.as_ref())?;
533 let sources_path = target_path.join(&metadata.contract_name);
534 metadata.source_tree().write_to(&target_path)?;
535
536 let mut settings = metadata.settings()?;
537
538 for remapping in &mut settings.remappings {
540 let new_path = sources_path.join(remapping.path.trim_start_matches('/'));
541 remapping.path = new_path.display().to_string();
542 }
543
544 if !settings.remappings.iter().any(|remapping| remapping.name.starts_with("@openzeppelin/")) {
546 let oz = Remapping {
547 context: None,
548 name: "@openzeppelin/".into(),
549 path: sources_path.join("@openzeppelin").display().to_string(),
550 };
551 settings.remappings.push(oz);
552 }
553
554 let paths = ProjectPathsConfig::builder()
558 .sources(sources_path.clone())
559 .remappings(settings.remappings.clone())
560 .build_with_root(sources_path);
561
562 let v = metadata.compiler_version()?;
563 let solc = Solc::find_or_install(&v)?;
564
565 let compiler = SolcCompiler::Specific(solc);
566
567 Ok(ProjectBuilder::<SolcCompiler>::default()
568 .settings(SolcSettings {
569 settings: SolcConfig::builder().settings(settings).build(),
570 ..Default::default()
571 })
572 .paths(paths)
573 .ephemeral()
574 .no_artifacts()
575 .build(compiler)?)
576}
577
578pub fn with_compilation_reporter<O>(quiet: bool, f: impl FnOnce() -> O) -> O {
580 #[expect(clippy::collapsible_else_if)]
581 let reporter = if quiet || shell::is_json() {
582 Report::new(NoReporter::default())
583 } else {
584 if std::io::stdout().is_terminal() {
585 Report::new(SpinnerReporter::spawn())
586 } else {
587 Report::new(BasicStdoutReporter::default())
588 }
589 };
590
591 foundry_compilers::report::with_scoped(&reporter, f)
592}
593
594#[derive(Clone, PartialEq, Eq)]
601pub enum PathOrContractInfo {
602 Path(PathBuf),
604 ContractInfo(CompilerContractInfo),
606}
607
608impl PathOrContractInfo {
609 pub fn path(&self) -> Option<PathBuf> {
611 match self {
612 Self::Path(path) => Some(path.to_path_buf()),
613 Self::ContractInfo(info) => info.path.as_ref().map(PathBuf::from),
614 }
615 }
616
617 pub fn name(&self) -> Option<&str> {
619 match self {
620 Self::Path(_) => None,
621 Self::ContractInfo(info) => Some(&info.name),
622 }
623 }
624}
625
626impl FromStr for PathOrContractInfo {
627 type Err = eyre::Error;
628
629 fn from_str(s: &str) -> Result<Self> {
630 if let Ok(contract) = CompilerContractInfo::from_str(s) {
631 return Ok(Self::ContractInfo(contract));
632 }
633 let path = PathBuf::from(s);
634 if path.extension().is_some_and(|ext| ext == "sol" || ext == "vy") {
635 return Ok(Self::Path(path));
636 }
637 Err(eyre::eyre!("Invalid contract identifier, file is not *.sol or *.vy: {}", s))
638 }
639}
640
641impl std::fmt::Debug for PathOrContractInfo {
642 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
643 match self {
644 Self::Path(path) => write!(f, "Path({})", path.display()),
645 Self::ContractInfo(info) => {
646 write!(f, "ContractInfo({info})")
647 }
648 }
649 }
650}
651
652#[cfg(test)]
653mod tests {
654 use super::*;
655
656 #[test]
657 fn parse_contract_identifiers() {
658 let t = ["src/Counter.sol", "src/Counter.sol:Counter", "Counter"];
659
660 let i1 = PathOrContractInfo::from_str(t[0]).unwrap();
661 assert_eq!(i1, PathOrContractInfo::Path(PathBuf::from(t[0])));
662
663 let i2 = PathOrContractInfo::from_str(t[1]).unwrap();
664 assert_eq!(
665 i2,
666 PathOrContractInfo::ContractInfo(CompilerContractInfo {
667 path: Some("src/Counter.sol".to_string()),
668 name: "Counter".to_string()
669 })
670 );
671
672 let i3 = PathOrContractInfo::from_str(t[2]).unwrap();
673 assert_eq!(
674 i3,
675 PathOrContractInfo::ContractInfo(CompilerContractInfo {
676 path: None,
677 name: "Counter".to_string()
678 })
679 );
680 }
681}