foundry_common/
compile.rs

1//! Support for compiling [foundry_compilers::Project]
2
3use crate::{
4    TestFunctionExt,
5    preprocessor::TestOptimizerPreprocessor,
6    reports::{ReportKind, report_kind},
7    shell,
8    term::SpinnerReporter,
9};
10use comfy_table::{Cell, Color, Table, modifiers::UTF8_ROUND_CORNERS};
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/// Builder type to configure how to compile a project.
36///
37/// This is merely a wrapper for [`Project::compile()`] which also prints to stdout depending on its
38/// settings.
39#[must_use = "ProjectCompiler does nothing unless you call a `compile*` method"]
40pub struct ProjectCompiler {
41    /// The root of the project.
42    project_root: PathBuf,
43
44    /// Whether we are going to verify the contracts after compilation.
45    verify: Option<bool>,
46
47    /// Whether to also print contract names.
48    print_names: Option<bool>,
49
50    /// Whether to also print contract sizes.
51    print_sizes: Option<bool>,
52
53    /// Whether to print anything at all. Overrides other `print` options.
54    quiet: Option<bool>,
55
56    /// Whether to bail on compiler errors.
57    bail: Option<bool>,
58
59    /// Whether to ignore the contract initcode size limit introduced by EIP-3860.
60    ignore_eip_3860: bool,
61
62    /// Extra files to include, that are not necessarily in the project's source dir.
63    files: Vec<PathBuf>,
64
65    /// Whether to compile with dynamic linking tests and scripts.
66    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    /// Create a new builder with the default settings.
78    #[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    /// Sets whether we are going to verify the contracts after compilation.
94    #[inline]
95    pub fn verify(mut self, yes: bool) -> Self {
96        self.verify = Some(yes);
97        self
98    }
99
100    /// Sets whether to print contract names.
101    #[inline]
102    pub fn print_names(mut self, yes: bool) -> Self {
103        self.print_names = Some(yes);
104        self
105    }
106
107    /// Sets whether to print contract sizes.
108    #[inline]
109    pub fn print_sizes(mut self, yes: bool) -> Self {
110        self.print_sizes = Some(yes);
111        self
112    }
113
114    /// Sets whether to print anything at all. Overrides other `print` options.
115    #[inline]
116    #[doc(alias = "silent")]
117    pub fn quiet(mut self, yes: bool) -> Self {
118        self.quiet = Some(yes);
119        self
120    }
121
122    /// Sets whether to bail on compiler errors.
123    #[inline]
124    pub fn bail(mut self, yes: bool) -> Self {
125        self.bail = Some(yes);
126        self
127    }
128
129    /// Sets whether to ignore EIP-3860 initcode size limits.
130    #[inline]
131    pub fn ignore_eip_3860(mut self, yes: bool) -> Self {
132        self.ignore_eip_3860 = yes;
133        self
134    }
135
136    /// Sets extra files to include, that are not necessarily in the project's source dir.
137    #[inline]
138    pub fn files(mut self, files: impl IntoIterator<Item = PathBuf>) -> Self {
139        self.files.extend(files);
140        self
141    }
142
143    /// Sets if tests should be dynamically linked.
144    #[inline]
145    pub fn dynamic_test_linking(mut self, preprocess: bool) -> Self {
146        self.dynamic_test_linking = preprocess;
147        self
148    }
149    /// Compiles the project.
150    pub fn compile<C: Compiler<CompilerContract = Contract>>(
151        mut self,
152        project: &Project<C>,
153    ) -> Result<ProjectCompileOutput<C>>
154    where
155        TestOptimizerPreprocessor: Preprocessor<C>,
156    {
157        self.project_root = project.root().to_path_buf();
158
159        // TODO: Avoid using std::process::exit(0).
160        // Replacing this with a return (e.g., Ok(ProjectCompileOutput::default())) would be more
161        // idiomatic, but it currently requires a `Default` bound on `C::Language`, which
162        // breaks compatibility with downstream crates like `foundry-cli`. This would need a
163        // broader refactor across the call chain. Leaving it as-is for now until a larger
164        // refactor is feasible.
165        if !project.paths.has_input_files() && self.files.is_empty() {
166            sh_println!("Nothing to compile")?;
167            std::process::exit(0);
168        }
169
170        // Taking is fine since we don't need these in `compile_with`.
171        let files = std::mem::take(&mut self.files);
172        let preprocess = self.dynamic_test_linking;
173        self.compile_with(|| {
174            let sources = if !files.is_empty() {
175                Source::read_all(files)?
176            } else {
177                project.paths.read_input_files()?
178            };
179
180            let mut compiler =
181                foundry_compilers::project::ProjectCompiler::with_sources(project, sources)?;
182            if preprocess {
183                compiler = compiler.with_preprocessor(TestOptimizerPreprocessor);
184            }
185            compiler.compile().map_err(Into::into)
186        })
187    }
188
189    /// Compiles the project with the given closure
190    ///
191    /// # Example
192    ///
193    /// ```ignore
194    /// use foundry_common::compile::ProjectCompiler;
195    /// let config = foundry_config::Config::load().unwrap();
196    /// let prj = config.project().unwrap();
197    /// ProjectCompiler::new().compile_with(|| Ok(prj.compile()?)).unwrap();
198    /// ```
199    #[instrument(target = "forge::compile", skip_all)]
200    fn compile_with<C: Compiler<CompilerContract = Contract>, F>(
201        self,
202        f: F,
203    ) -> Result<ProjectCompileOutput<C>>
204    where
205        F: FnOnce() -> Result<ProjectCompileOutput<C>>,
206    {
207        let quiet = self.quiet.unwrap_or(false);
208        let bail = self.bail.unwrap_or(true);
209
210        let output = with_compilation_reporter(quiet, || {
211            tracing::debug!("compiling project");
212
213            let timer = Instant::now();
214            let r = f();
215            let elapsed = timer.elapsed();
216
217            tracing::debug!("finished compiling in {:.3}s", elapsed.as_secs_f64());
218            r
219        })?;
220
221        if bail && output.has_compiler_errors() {
222            eyre::bail!("{output}")
223        }
224
225        if !quiet {
226            if !shell::is_json() {
227                if output.is_unchanged() {
228                    sh_println!("No files changed, compilation skipped")?;
229                } else {
230                    // print the compiler output / warnings
231                    sh_println!("{output}")?;
232                }
233            }
234
235            self.handle_output(&output)?;
236        }
237
238        Ok(output)
239    }
240
241    /// If configured, this will print sizes or names
242    fn handle_output<C: Compiler<CompilerContract = Contract>>(
243        &self,
244        output: &ProjectCompileOutput<C>,
245    ) -> Result<()> {
246        let print_names = self.print_names.unwrap_or(false);
247        let print_sizes = self.print_sizes.unwrap_or(false);
248
249        // print any sizes or names
250        if print_names {
251            let mut artifacts: BTreeMap<_, Vec<_>> = BTreeMap::new();
252            for (name, (_, version)) in output.versioned_artifacts() {
253                artifacts.entry(version).or_default().push(name);
254            }
255
256            if shell::is_json() {
257                sh_println!("{}", serde_json::to_string(&artifacts).unwrap())?;
258            } else {
259                for (version, names) in artifacts {
260                    sh_println!(
261                        "  compiler version: {}.{}.{}",
262                        version.major,
263                        version.minor,
264                        version.patch
265                    )?;
266                    for name in names {
267                        sh_println!("    - {name}")?;
268                    }
269                }
270            }
271        }
272
273        if print_sizes {
274            // add extra newline if names were already printed
275            if print_names && !shell::is_json() {
276                sh_println!()?;
277            }
278
279            let mut size_report =
280                SizeReport { report_kind: report_kind(), contracts: BTreeMap::new() };
281
282            let mut artifacts: BTreeMap<String, Vec<_>> = BTreeMap::new();
283            for (id, artifact) in output.artifact_ids().filter(|(id, _)| {
284                // filter out forge-std specific contracts
285                !id.source.to_string_lossy().contains("/forge-std/src/")
286            }) {
287                artifacts.entry(id.name.clone()).or_default().push((id.source.clone(), artifact));
288            }
289
290            for (name, artifact_list) in artifacts {
291                for (path, artifact) in &artifact_list {
292                    let runtime_size = contract_size(*artifact, false).unwrap_or_default();
293                    let init_size = contract_size(*artifact, true).unwrap_or_default();
294
295                    let is_dev_contract = artifact
296                        .abi
297                        .as_ref()
298                        .map(|abi| {
299                            abi.functions().any(|f| {
300                                f.test_function_kind().is_known()
301                                    || matches!(f.name.as_str(), "IS_TEST" | "IS_SCRIPT")
302                            })
303                        })
304                        .unwrap_or(false);
305
306                    let unique_name = if artifact_list.len() > 1 {
307                        format!(
308                            "{} ({})",
309                            name,
310                            path.strip_prefix(&self.project_root).unwrap_or(path).display()
311                        )
312                    } else {
313                        name.clone()
314                    };
315
316                    size_report.contracts.insert(
317                        unique_name,
318                        ContractInfo { runtime_size, init_size, is_dev_contract },
319                    );
320                }
321            }
322
323            sh_println!("{size_report}")?;
324
325            eyre::ensure!(
326                !size_report.exceeds_runtime_size_limit(),
327                "some contracts exceed the runtime size limit \
328                 (EIP-170: {CONTRACT_RUNTIME_SIZE_LIMIT} bytes)"
329            );
330            // Check size limits only if not ignoring EIP-3860
331            eyre::ensure!(
332                self.ignore_eip_3860 || !size_report.exceeds_initcode_size_limit(),
333                "some contracts exceed the initcode size limit \
334                 (EIP-3860: {CONTRACT_INITCODE_SIZE_LIMIT} bytes)"
335            );
336        }
337
338        Ok(())
339    }
340}
341
342// https://eips.ethereum.org/EIPS/eip-170
343const CONTRACT_RUNTIME_SIZE_LIMIT: usize = 24576;
344
345// https://eips.ethereum.org/EIPS/eip-3860
346const CONTRACT_INITCODE_SIZE_LIMIT: usize = 49152;
347
348/// Contracts with info about their size
349pub struct SizeReport {
350    /// What kind of report to generate.
351    report_kind: ReportKind,
352    /// `contract name -> info`
353    pub contracts: BTreeMap<String, ContractInfo>,
354}
355
356impl SizeReport {
357    /// Returns the maximum runtime code size, excluding dev contracts.
358    pub fn max_runtime_size(&self) -> usize {
359        self.contracts
360            .values()
361            .filter(|c| !c.is_dev_contract)
362            .map(|c| c.runtime_size)
363            .max()
364            .unwrap_or(0)
365    }
366
367    /// Returns the maximum initcode size, excluding dev contracts.
368    pub fn max_init_size(&self) -> usize {
369        self.contracts
370            .values()
371            .filter(|c| !c.is_dev_contract)
372            .map(|c| c.init_size)
373            .max()
374            .unwrap_or(0)
375    }
376
377    /// Returns true if any contract exceeds the runtime size limit, excluding dev contracts.
378    pub fn exceeds_runtime_size_limit(&self) -> bool {
379        self.max_runtime_size() > CONTRACT_RUNTIME_SIZE_LIMIT
380    }
381
382    /// Returns true if any contract exceeds the initcode size limit, excluding dev contracts.
383    pub fn exceeds_initcode_size_limit(&self) -> bool {
384        self.max_init_size() > CONTRACT_INITCODE_SIZE_LIMIT
385    }
386}
387
388impl Display for SizeReport {
389    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
390        match self.report_kind {
391            ReportKind::Text => {
392                writeln!(f, "\n{}", self.format_table_output())?;
393            }
394            ReportKind::JSON => {
395                writeln!(f, "{}", self.format_json_output())?;
396            }
397        }
398
399        Ok(())
400    }
401}
402
403impl SizeReport {
404    fn format_json_output(&self) -> String {
405        let contracts = self
406            .contracts
407            .iter()
408            .filter(|(_, c)| !c.is_dev_contract && (c.runtime_size > 0 || c.init_size > 0))
409            .map(|(name, contract)| {
410                (
411                    name.clone(),
412                    serde_json::json!({
413                        "runtime_size": contract.runtime_size,
414                        "init_size": contract.init_size,
415                        "runtime_margin": CONTRACT_RUNTIME_SIZE_LIMIT as isize - contract.runtime_size as isize,
416                        "init_margin": CONTRACT_INITCODE_SIZE_LIMIT as isize - contract.init_size as isize,
417                    }),
418                )
419            })
420            .collect::<serde_json::Map<_, _>>();
421
422        serde_json::to_string(&contracts).unwrap()
423    }
424
425    fn format_table_output(&self) -> Table {
426        let mut table = Table::new();
427        table.apply_modifier(UTF8_ROUND_CORNERS);
428
429        table.set_header(vec![
430            Cell::new("Contract"),
431            Cell::new("Runtime Size (B)"),
432            Cell::new("Initcode Size (B)"),
433            Cell::new("Runtime Margin (B)"),
434            Cell::new("Initcode Margin (B)"),
435        ]);
436
437        // Filters out dev contracts (Test or Script)
438        let contracts = self
439            .contracts
440            .iter()
441            .filter(|(_, c)| !c.is_dev_contract && (c.runtime_size > 0 || c.init_size > 0));
442        for (name, contract) in contracts {
443            let runtime_margin =
444                CONTRACT_RUNTIME_SIZE_LIMIT as isize - contract.runtime_size as isize;
445            let init_margin = CONTRACT_INITCODE_SIZE_LIMIT as isize - contract.init_size as isize;
446
447            let runtime_color = match contract.runtime_size {
448                ..18_000 => Color::Reset,
449                18_000..=CONTRACT_RUNTIME_SIZE_LIMIT => Color::Yellow,
450                _ => Color::Red,
451            };
452
453            let init_color = match contract.init_size {
454                ..36_000 => Color::Reset,
455                36_000..=CONTRACT_INITCODE_SIZE_LIMIT => Color::Yellow,
456                _ => Color::Red,
457            };
458
459            let locale = &Locale::en;
460            table.add_row([
461                Cell::new(name),
462                Cell::new(contract.runtime_size.to_formatted_string(locale)).fg(runtime_color),
463                Cell::new(contract.init_size.to_formatted_string(locale)).fg(init_color),
464                Cell::new(runtime_margin.to_formatted_string(locale)).fg(runtime_color),
465                Cell::new(init_margin.to_formatted_string(locale)).fg(init_color),
466            ]);
467        }
468
469        table
470    }
471}
472
473/// Returns the deployed or init size of the contract.
474fn contract_size<T: Artifact>(artifact: &T, initcode: bool) -> Option<usize> {
475    let bytecode = if initcode {
476        artifact.get_bytecode_object()?
477    } else {
478        artifact.get_deployed_bytecode_object()?
479    };
480
481    let size = match bytecode.as_ref() {
482        BytecodeObject::Bytecode(bytes) => bytes.len(),
483        BytecodeObject::Unlinked(unlinked) => {
484            // we don't need to account for placeholders here, because library placeholders take up
485            // 40 characters: `__$<library hash>$__` which is the same as a 20byte address in hex.
486            let mut size = unlinked.len();
487            if unlinked.starts_with("0x") {
488                size -= 2;
489            }
490            // hex -> bytes
491            size / 2
492        }
493    };
494
495    Some(size)
496}
497
498/// How big the contract is and whether it is a dev contract where size limits can be neglected
499#[derive(Clone, Copy, Debug)]
500pub struct ContractInfo {
501    /// Size of the runtime code in bytes
502    pub runtime_size: usize,
503    /// Size of the initcode in bytes
504    pub init_size: usize,
505    /// A development contract is either a Script or a Test contract.
506    pub is_dev_contract: bool,
507}
508
509/// Compiles target file path.
510///
511/// If `quiet` no solc related output will be emitted to stdout.
512///
513/// If `verify` and it's a standalone script, throw error. Only allowed for projects.
514///
515/// **Note:** this expects the `target_path` to be absolute
516pub fn compile_target<C: Compiler<CompilerContract = Contract>>(
517    target_path: &Path,
518    project: &Project<C>,
519    quiet: bool,
520) -> Result<ProjectCompileOutput<C>>
521where
522    TestOptimizerPreprocessor: Preprocessor<C>,
523{
524    ProjectCompiler::new().quiet(quiet).files([target_path.into()]).compile(project)
525}
526
527/// Creates a [Project] from an Etherscan source.
528pub fn etherscan_project(
529    metadata: &Metadata,
530    target_path: impl AsRef<Path>,
531) -> Result<Project<SolcCompiler>> {
532    let target_path = dunce::canonicalize(target_path.as_ref())?;
533    let sources_path = target_path.join(&metadata.contract_name);
534    metadata.source_tree().write_to(&target_path)?;
535
536    let mut settings = metadata.settings()?;
537
538    // make remappings absolute with our root
539    for remapping in &mut settings.remappings {
540        let new_path = sources_path.join(remapping.path.trim_start_matches('/'));
541        remapping.path = new_path.display().to_string();
542    }
543
544    // add missing remappings
545    if !settings.remappings.iter().any(|remapping| remapping.name.starts_with("@openzeppelin/")) {
546        let oz = Remapping {
547            context: None,
548            name: "@openzeppelin/".into(),
549            path: sources_path.join("@openzeppelin").display().to_string(),
550        };
551        settings.remappings.push(oz);
552    }
553
554    // root/
555    //   ContractName/
556    //     [source code]
557    let paths = ProjectPathsConfig::builder()
558        .sources(sources_path.clone())
559        .remappings(settings.remappings.clone())
560        .build_with_root(sources_path);
561
562    let v = metadata.compiler_version()?;
563    let solc = Solc::find_or_install(&v)?;
564
565    let compiler = SolcCompiler::Specific(solc);
566
567    Ok(ProjectBuilder::<SolcCompiler>::default()
568        .settings(SolcSettings {
569            settings: SolcConfig::builder().settings(settings).build(),
570            ..Default::default()
571        })
572        .paths(paths)
573        .ephemeral()
574        .no_artifacts()
575        .build(compiler)?)
576}
577
578/// Configures the reporter and runs the given closure.
579pub fn with_compilation_reporter<O>(quiet: bool, f: impl FnOnce() -> O) -> O {
580    #[expect(clippy::collapsible_else_if)]
581    let reporter = if quiet || shell::is_json() {
582        Report::new(NoReporter::default())
583    } else {
584        if std::io::stdout().is_terminal() {
585            Report::new(SpinnerReporter::spawn())
586        } else {
587            Report::new(BasicStdoutReporter::default())
588        }
589    };
590
591    foundry_compilers::report::with_scoped(&reporter, f)
592}
593
594/// Container type for parsing contract identifiers from CLI.
595///
596/// Passed string can be of the following forms:
597/// - `src/Counter.sol` - path to the contract file, in the case where it only contains one contract
598/// - `src/Counter.sol:Counter` - path to the contract file and the contract name
599/// - `Counter` - contract name only
600#[derive(Clone, PartialEq, Eq)]
601pub enum PathOrContractInfo {
602    /// Non-canoncalized path provided via CLI.
603    Path(PathBuf),
604    /// Contract info provided via CLI.
605    ContractInfo(CompilerContractInfo),
606}
607
608impl PathOrContractInfo {
609    /// Returns the path to the contract file if provided.
610    pub fn path(&self) -> Option<PathBuf> {
611        match self {
612            Self::Path(path) => Some(path.to_path_buf()),
613            Self::ContractInfo(info) => info.path.as_ref().map(PathBuf::from),
614        }
615    }
616
617    /// Returns the contract name if provided.
618    pub fn name(&self) -> Option<&str> {
619        match self {
620            Self::Path(_) => None,
621            Self::ContractInfo(info) => Some(&info.name),
622        }
623    }
624}
625
626impl FromStr for PathOrContractInfo {
627    type Err = eyre::Error;
628
629    fn from_str(s: &str) -> Result<Self> {
630        if let Ok(contract) = CompilerContractInfo::from_str(s) {
631            return Ok(Self::ContractInfo(contract));
632        }
633        let path = PathBuf::from(s);
634        if path.extension().is_some_and(|ext| ext == "sol" || ext == "vy") {
635            return Ok(Self::Path(path));
636        }
637        Err(eyre::eyre!("Invalid contract identifier, file is not *.sol or *.vy: {}", s))
638    }
639}
640
641impl std::fmt::Debug for PathOrContractInfo {
642    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
643        match self {
644            Self::Path(path) => write!(f, "Path({})", path.display()),
645            Self::ContractInfo(info) => {
646                write!(f, "ContractInfo({info})")
647            }
648        }
649    }
650}
651
652#[cfg(test)]
653mod tests {
654    use super::*;
655
656    #[test]
657    fn parse_contract_identifiers() {
658        let t = ["src/Counter.sol", "src/Counter.sol:Counter", "Counter"];
659
660        let i1 = PathOrContractInfo::from_str(t[0]).unwrap();
661        assert_eq!(i1, PathOrContractInfo::Path(PathBuf::from(t[0])));
662
663        let i2 = PathOrContractInfo::from_str(t[1]).unwrap();
664        assert_eq!(
665            i2,
666            PathOrContractInfo::ContractInfo(CompilerContractInfo {
667                path: Some("src/Counter.sol".to_string()),
668                name: "Counter".to_string()
669            })
670        );
671
672        let i3 = PathOrContractInfo::from_str(t[2]).unwrap();
673        assert_eq!(
674            i3,
675            PathOrContractInfo::ContractInfo(CompilerContractInfo {
676                path: None,
677                name: "Counter".to_string()
678            })
679        );
680    }
681}