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