1use crate::{
4 TestFunctionExt,
5 preprocessor::DynamicTestLinkingPreprocessor,
6 reports::{ReportKind, report_kind},
7 shell,
8 term::SpinnerReporter,
9};
10use comfy_table::{Cell, Color, Table, modifiers::UTF8_ROUND_CORNERS, presets::ASCII_MARKDOWN};
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
150 #[instrument(target = "forge::compile", skip_all)]
152 pub fn compile<C: Compiler<CompilerContract = Contract>>(
153 mut self,
154 project: &Project<C>,
155 ) -> Result<ProjectCompileOutput<C>>
156 where
157 DynamicTestLinkingPreprocessor: Preprocessor<C>,
158 {
159 self.project_root = project.root().to_path_buf();
160
161 if !project.paths.has_input_files() && self.files.is_empty() {
168 sh_println!("Nothing to compile")?;
169 std::process::exit(0);
170 }
171
172 let files = std::mem::take(&mut self.files);
174 let preprocess = self.dynamic_test_linking;
175 self.compile_with(|| {
176 let sources = if !files.is_empty() {
177 Source::read_all(files)?
178 } else {
179 project.paths.read_input_files()?
180 };
181
182 let mut compiler =
183 foundry_compilers::project::ProjectCompiler::with_sources(project, sources)?;
184 if preprocess {
185 compiler = compiler.with_preprocessor(DynamicTestLinkingPreprocessor);
186 }
187 compiler.compile().map_err(Into::into)
188 })
189 }
190
191 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(false))?;
394 }
395 ReportKind::JSON => {
396 writeln!(f, "{}", self.format_json_output())?;
397 }
398 ReportKind::Markdown => {
399 writeln!(f, "\n{}", self.format_table_output(true))?;
400 }
401 }
402
403 Ok(())
404 }
405}
406
407impl SizeReport {
408 fn format_json_output(&self) -> String {
409 let contracts = self
410 .contracts
411 .iter()
412 .filter(|(_, c)| !c.is_dev_contract && (c.runtime_size > 0 || c.init_size > 0))
413 .map(|(name, contract)| {
414 (
415 name.clone(),
416 serde_json::json!({
417 "runtime_size": contract.runtime_size,
418 "init_size": contract.init_size,
419 "runtime_margin": CONTRACT_RUNTIME_SIZE_LIMIT as isize - contract.runtime_size as isize,
420 "init_margin": CONTRACT_INITCODE_SIZE_LIMIT as isize - contract.init_size as isize,
421 }),
422 )
423 })
424 .collect::<serde_json::Map<_, _>>();
425
426 serde_json::to_string(&contracts).unwrap()
427 }
428
429 fn format_table_output(&self, md: bool) -> Table {
430 let mut table = Table::new();
431 if md {
432 table.load_preset(ASCII_MARKDOWN);
433 } else {
434 table.apply_modifier(UTF8_ROUND_CORNERS);
435 }
436
437 table.set_header(vec![
438 Cell::new("Contract"),
439 Cell::new("Runtime Size (B)"),
440 Cell::new("Initcode Size (B)"),
441 Cell::new("Runtime Margin (B)"),
442 Cell::new("Initcode Margin (B)"),
443 ]);
444
445 let contracts = self
447 .contracts
448 .iter()
449 .filter(|(_, c)| !c.is_dev_contract && (c.runtime_size > 0 || c.init_size > 0));
450 for (name, contract) in contracts {
451 let runtime_margin =
452 CONTRACT_RUNTIME_SIZE_LIMIT as isize - contract.runtime_size as isize;
453 let init_margin = CONTRACT_INITCODE_SIZE_LIMIT as isize - contract.init_size as isize;
454
455 let runtime_color = match contract.runtime_size {
456 ..18_000 => Color::Reset,
457 18_000..=CONTRACT_RUNTIME_SIZE_LIMIT => Color::Yellow,
458 _ => Color::Red,
459 };
460
461 let init_color = match contract.init_size {
462 ..36_000 => Color::Reset,
463 36_000..=CONTRACT_INITCODE_SIZE_LIMIT => Color::Yellow,
464 _ => Color::Red,
465 };
466
467 let locale = &Locale::en;
468 table.add_row([
469 Cell::new(name),
470 Cell::new(contract.runtime_size.to_formatted_string(locale)).fg(runtime_color),
471 Cell::new(contract.init_size.to_formatted_string(locale)).fg(init_color),
472 Cell::new(runtime_margin.to_formatted_string(locale)).fg(runtime_color),
473 Cell::new(init_margin.to_formatted_string(locale)).fg(init_color),
474 ]);
475 }
476
477 table
478 }
479}
480
481fn contract_size<T: Artifact>(artifact: &T, initcode: bool) -> Option<usize> {
483 let bytecode = if initcode {
484 artifact.get_bytecode_object()?
485 } else {
486 artifact.get_deployed_bytecode_object()?
487 };
488
489 let size = match bytecode.as_ref() {
490 BytecodeObject::Bytecode(bytes) => bytes.len(),
491 BytecodeObject::Unlinked(unlinked) => {
492 let mut size = unlinked.len();
495 if unlinked.starts_with("0x") {
496 size -= 2;
497 }
498 size / 2
500 }
501 };
502
503 Some(size)
504}
505
506#[derive(Clone, Copy, Debug)]
508pub struct ContractInfo {
509 pub runtime_size: usize,
511 pub init_size: usize,
513 pub is_dev_contract: bool,
515}
516
517pub fn compile_target<C: Compiler<CompilerContract = Contract>>(
525 target_path: &Path,
526 project: &Project<C>,
527 quiet: bool,
528) -> Result<ProjectCompileOutput<C>>
529where
530 DynamicTestLinkingPreprocessor: Preprocessor<C>,
531{
532 ProjectCompiler::new().quiet(quiet).files([target_path.into()]).compile(project)
533}
534
535pub fn etherscan_project(
537 metadata: &Metadata,
538 target_path: impl AsRef<Path>,
539) -> Result<Project<SolcCompiler>> {
540 let target_path = dunce::canonicalize(target_path.as_ref())?;
541 let sources_path = target_path.join(&metadata.contract_name);
542 metadata.source_tree().write_to(&target_path)?;
543
544 let mut settings = metadata.settings()?;
545
546 for remapping in &mut settings.remappings {
548 let new_path = sources_path.join(remapping.path.trim_start_matches('/'));
549 remapping.path = new_path.display().to_string();
550 }
551
552 if !settings.remappings.iter().any(|remapping| remapping.name.starts_with("@openzeppelin/")) {
554 let oz = Remapping {
555 context: None,
556 name: "@openzeppelin/".into(),
557 path: sources_path.join("@openzeppelin").display().to_string(),
558 };
559 settings.remappings.push(oz);
560 }
561
562 let paths = ProjectPathsConfig::builder()
566 .sources(sources_path.clone())
567 .remappings(settings.remappings.clone())
568 .build_with_root(sources_path);
569
570 let v = metadata.compiler_version()?;
571 let solc = Solc::find_or_install(&v)?;
572
573 let compiler = SolcCompiler::Specific(solc);
574
575 Ok(ProjectBuilder::<SolcCompiler>::default()
576 .settings(SolcSettings {
577 settings: SolcConfig::builder().settings(settings).build(),
578 ..Default::default()
579 })
580 .paths(paths)
581 .ephemeral()
582 .no_artifacts()
583 .build(compiler)?)
584}
585
586pub fn with_compilation_reporter<O>(quiet: bool, f: impl FnOnce() -> O) -> O {
588 #[expect(clippy::collapsible_else_if)]
589 let reporter = if quiet || shell::is_json() {
590 Report::new(NoReporter::default())
591 } else {
592 if std::io::stdout().is_terminal() {
593 Report::new(SpinnerReporter::spawn())
594 } else {
595 Report::new(BasicStdoutReporter::default())
596 }
597 };
598
599 foundry_compilers::report::with_scoped(&reporter, f)
600}
601
602#[derive(Clone, PartialEq, Eq)]
609pub enum PathOrContractInfo {
610 Path(PathBuf),
612 ContractInfo(CompilerContractInfo),
614}
615
616impl PathOrContractInfo {
617 pub fn path(&self) -> Option<PathBuf> {
619 match self {
620 Self::Path(path) => Some(path.to_path_buf()),
621 Self::ContractInfo(info) => info.path.as_ref().map(PathBuf::from),
622 }
623 }
624
625 pub fn name(&self) -> Option<&str> {
627 match self {
628 Self::Path(_) => None,
629 Self::ContractInfo(info) => Some(&info.name),
630 }
631 }
632}
633
634impl FromStr for PathOrContractInfo {
635 type Err = eyre::Error;
636
637 fn from_str(s: &str) -> Result<Self> {
638 if let Ok(contract) = CompilerContractInfo::from_str(s) {
639 return Ok(Self::ContractInfo(contract));
640 }
641 let path = PathBuf::from(s);
642 if path.extension().is_some_and(|ext| ext == "sol" || ext == "vy") {
643 return Ok(Self::Path(path));
644 }
645 Err(eyre::eyre!("Invalid contract identifier, file is not *.sol or *.vy: {}", s))
646 }
647}
648
649impl std::fmt::Debug for PathOrContractInfo {
650 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
651 match self {
652 Self::Path(path) => write!(f, "Path({})", path.display()),
653 Self::ContractInfo(info) => {
654 write!(f, "ContractInfo({info})")
655 }
656 }
657 }
658}
659
660#[cfg(test)]
661mod tests {
662 use super::*;
663
664 #[test]
665 fn parse_contract_identifiers() {
666 let t = ["src/Counter.sol", "src/Counter.sol:Counter", "Counter"];
667
668 let i1 = PathOrContractInfo::from_str(t[0]).unwrap();
669 assert_eq!(i1, PathOrContractInfo::Path(PathBuf::from(t[0])));
670
671 let i2 = PathOrContractInfo::from_str(t[1]).unwrap();
672 assert_eq!(
673 i2,
674 PathOrContractInfo::ContractInfo(CompilerContractInfo {
675 path: Some("src/Counter.sol".to_string()),
676 name: "Counter".to_string()
677 })
678 );
679
680 let i3 = PathOrContractInfo::from_str(t[2]).unwrap();
681 assert_eq!(
682 i3,
683 PathOrContractInfo::ContractInfo(CompilerContractInfo {
684 path: None,
685 name: "Counter".to_string()
686 })
687 );
688 }
689}