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