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