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