foundry_common/
compile.rs

1//! Support for compiling [foundry_compilers::Project]
2
3use 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/// 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
150    /// Compiles the project.
151    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        // TODO: Avoid using std::process::exit(0).
161        // Replacing this with a return (e.g., Ok(ProjectCompileOutput::default())) would be more
162        // idiomatic, but it currently requires a `Default` bound on `C::Language`, which
163        // breaks compatibility with downstream crates like `foundry-cli`. This would need a
164        // broader refactor across the call chain. Leaving it as-is for now until a larger
165        // refactor is feasible.
166        if !project.paths.has_input_files() && self.files.is_empty() {
167            sh_println!("Nothing to compile")?;
168            std::process::exit(0);
169        }
170
171        // Taking is fine since we don't need these in `compile_with`.
172        let files = std::mem::take(&mut self.files);
173        let preprocess = self.dynamic_test_linking;
174        self.compile_with(|| {
175            let sources = if !files.is_empty() {
176                Source::read_all(files)?
177            } else {
178                project.paths.read_input_files()?
179            };
180
181            let mut compiler =
182                foundry_compilers::project::ProjectCompiler::with_sources(project, sources)?;
183            if preprocess {
184                compiler = compiler.with_preprocessor(TestOptimizerPreprocessor);
185            }
186            compiler.compile().map_err(Into::into)
187        })
188    }
189
190    /// Compiles the project with the given closure
191    ///
192    /// # Example
193    ///
194    /// ```ignore
195    /// use foundry_common::compile::ProjectCompiler;
196    /// let config = foundry_config::Config::load().unwrap();
197    /// let prj = config.project().unwrap();
198    /// ProjectCompiler::new().compile_with(|| Ok(prj.compile()?)).unwrap();
199    /// ```
200    #[instrument(target = "forge::compile", skip_all)]
201    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                    // print the compiler output / warnings
232                    sh_println!("{output}")?;
233                }
234            }
235
236            self.handle_output(&output)?;
237        }
238
239        Ok(output)
240    }
241
242    /// If configured, this will print sizes or names
243    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        // print any sizes or names
251        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            // add extra newline if names were already printed
276            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                // filter out forge-std specific contracts
286                !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            // Check size limits only if not ignoring EIP-3860
332            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
343// https://eips.ethereum.org/EIPS/eip-170
344const CONTRACT_RUNTIME_SIZE_LIMIT: usize = 24576;
345
346// https://eips.ethereum.org/EIPS/eip-3860
347const CONTRACT_INITCODE_SIZE_LIMIT: usize = 49152;
348
349/// Contracts with info about their size
350pub struct SizeReport {
351    /// What kind of report to generate.
352    report_kind: ReportKind,
353    /// `contract name -> info`
354    pub contracts: BTreeMap<String, ContractInfo>,
355}
356
357impl SizeReport {
358    /// Returns the maximum runtime code size, excluding dev contracts.
359    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    /// Returns the maximum initcode size, excluding dev contracts.
369    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    /// Returns true if any contract exceeds the runtime size limit, excluding dev contracts.
379    pub fn exceeds_runtime_size_limit(&self) -> bool {
380        self.max_runtime_size() > CONTRACT_RUNTIME_SIZE_LIMIT
381    }
382
383    /// Returns true if any contract exceeds the initcode size limit, excluding dev contracts.
384    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())?;
394            }
395            ReportKind::JSON => {
396                writeln!(f, "{}", self.format_json_output())?;
397            }
398        }
399
400        Ok(())
401    }
402}
403
404impl SizeReport {
405    fn format_json_output(&self) -> String {
406        let contracts = self
407            .contracts
408            .iter()
409            .filter(|(_, c)| !c.is_dev_contract && (c.runtime_size > 0 || c.init_size > 0))
410            .map(|(name, contract)| {
411                (
412                    name.clone(),
413                    serde_json::json!({
414                        "runtime_size": contract.runtime_size,
415                        "init_size": contract.init_size,
416                        "runtime_margin": CONTRACT_RUNTIME_SIZE_LIMIT as isize - contract.runtime_size as isize,
417                        "init_margin": CONTRACT_INITCODE_SIZE_LIMIT as isize - contract.init_size as isize,
418                    }),
419                )
420            })
421            .collect::<serde_json::Map<_, _>>();
422
423        serde_json::to_string(&contracts).unwrap()
424    }
425
426    fn format_table_output(&self) -> Table {
427        let mut table = Table::new();
428        table.apply_modifier(UTF8_ROUND_CORNERS);
429
430        table.set_header(vec![
431            Cell::new("Contract"),
432            Cell::new("Runtime Size (B)"),
433            Cell::new("Initcode Size (B)"),
434            Cell::new("Runtime Margin (B)"),
435            Cell::new("Initcode Margin (B)"),
436        ]);
437
438        // Filters out dev contracts (Test or Script)
439        let contracts = self
440            .contracts
441            .iter()
442            .filter(|(_, c)| !c.is_dev_contract && (c.runtime_size > 0 || c.init_size > 0));
443        for (name, contract) in contracts {
444            let runtime_margin =
445                CONTRACT_RUNTIME_SIZE_LIMIT as isize - contract.runtime_size as isize;
446            let init_margin = CONTRACT_INITCODE_SIZE_LIMIT as isize - contract.init_size as isize;
447
448            let runtime_color = match contract.runtime_size {
449                ..18_000 => Color::Reset,
450                18_000..=CONTRACT_RUNTIME_SIZE_LIMIT => Color::Yellow,
451                _ => Color::Red,
452            };
453
454            let init_color = match contract.init_size {
455                ..36_000 => Color::Reset,
456                36_000..=CONTRACT_INITCODE_SIZE_LIMIT => Color::Yellow,
457                _ => Color::Red,
458            };
459
460            let locale = &Locale::en;
461            table.add_row([
462                Cell::new(name),
463                Cell::new(contract.runtime_size.to_formatted_string(locale)).fg(runtime_color),
464                Cell::new(contract.init_size.to_formatted_string(locale)).fg(init_color),
465                Cell::new(runtime_margin.to_formatted_string(locale)).fg(runtime_color),
466                Cell::new(init_margin.to_formatted_string(locale)).fg(init_color),
467            ]);
468        }
469
470        table
471    }
472}
473
474/// Returns the deployed or init size of the contract.
475fn contract_size<T: Artifact>(artifact: &T, initcode: bool) -> Option<usize> {
476    let bytecode = if initcode {
477        artifact.get_bytecode_object()?
478    } else {
479        artifact.get_deployed_bytecode_object()?
480    };
481
482    let size = match bytecode.as_ref() {
483        BytecodeObject::Bytecode(bytes) => bytes.len(),
484        BytecodeObject::Unlinked(unlinked) => {
485            // we don't need to account for placeholders here, because library placeholders take up
486            // 40 characters: `__$<library hash>$__` which is the same as a 20byte address in hex.
487            let mut size = unlinked.len();
488            if unlinked.starts_with("0x") {
489                size -= 2;
490            }
491            // hex -> bytes
492            size / 2
493        }
494    };
495
496    Some(size)
497}
498
499/// How big the contract is and whether it is a dev contract where size limits can be neglected
500#[derive(Clone, Copy, Debug)]
501pub struct ContractInfo {
502    /// Size of the runtime code in bytes
503    pub runtime_size: usize,
504    /// Size of the initcode in bytes
505    pub init_size: usize,
506    /// A development contract is either a Script or a Test contract.
507    pub is_dev_contract: bool,
508}
509
510/// Compiles target file path.
511///
512/// If `quiet` no solc related output will be emitted to stdout.
513///
514/// If `verify` and it's a standalone script, throw error. Only allowed for projects.
515///
516/// **Note:** this expects the `target_path` to be absolute
517pub fn compile_target<C: Compiler<CompilerContract = Contract>>(
518    target_path: &Path,
519    project: &Project<C>,
520    quiet: bool,
521) -> Result<ProjectCompileOutput<C>>
522where
523    TestOptimizerPreprocessor: Preprocessor<C>,
524{
525    ProjectCompiler::new().quiet(quiet).files([target_path.into()]).compile(project)
526}
527
528/// Creates a [Project] from an Etherscan source.
529pub fn etherscan_project(
530    metadata: &Metadata,
531    target_path: impl AsRef<Path>,
532) -> Result<Project<SolcCompiler>> {
533    let target_path = dunce::canonicalize(target_path.as_ref())?;
534    let sources_path = target_path.join(&metadata.contract_name);
535    metadata.source_tree().write_to(&target_path)?;
536
537    let mut settings = metadata.settings()?;
538
539    // make remappings absolute with our root
540    for remapping in &mut settings.remappings {
541        let new_path = sources_path.join(remapping.path.trim_start_matches('/'));
542        remapping.path = new_path.display().to_string();
543    }
544
545    // add missing remappings
546    if !settings.remappings.iter().any(|remapping| remapping.name.starts_with("@openzeppelin/")) {
547        let oz = Remapping {
548            context: None,
549            name: "@openzeppelin/".into(),
550            path: sources_path.join("@openzeppelin").display().to_string(),
551        };
552        settings.remappings.push(oz);
553    }
554
555    // root/
556    //   ContractName/
557    //     [source code]
558    let paths = ProjectPathsConfig::builder()
559        .sources(sources_path.clone())
560        .remappings(settings.remappings.clone())
561        .build_with_root(sources_path);
562
563    let v = metadata.compiler_version()?;
564    let solc = Solc::find_or_install(&v)?;
565
566    let compiler = SolcCompiler::Specific(solc);
567
568    Ok(ProjectBuilder::<SolcCompiler>::default()
569        .settings(SolcSettings {
570            settings: SolcConfig::builder().settings(settings).build(),
571            ..Default::default()
572        })
573        .paths(paths)
574        .ephemeral()
575        .no_artifacts()
576        .build(compiler)?)
577}
578
579/// Configures the reporter and runs the given closure.
580pub fn with_compilation_reporter<O>(quiet: bool, f: impl FnOnce() -> O) -> O {
581    #[expect(clippy::collapsible_else_if)]
582    let reporter = if quiet || shell::is_json() {
583        Report::new(NoReporter::default())
584    } else {
585        if std::io::stdout().is_terminal() {
586            Report::new(SpinnerReporter::spawn())
587        } else {
588            Report::new(BasicStdoutReporter::default())
589        }
590    };
591
592    foundry_compilers::report::with_scoped(&reporter, f)
593}
594
595/// Container type for parsing contract identifiers from CLI.
596///
597/// Passed string can be of the following forms:
598/// - `src/Counter.sol` - path to the contract file, in the case where it only contains one contract
599/// - `src/Counter.sol:Counter` - path to the contract file and the contract name
600/// - `Counter` - contract name only
601#[derive(Clone, PartialEq, Eq)]
602pub enum PathOrContractInfo {
603    /// Non-canoncalized path provided via CLI.
604    Path(PathBuf),
605    /// Contract info provided via CLI.
606    ContractInfo(CompilerContractInfo),
607}
608
609impl PathOrContractInfo {
610    /// Returns the path to the contract file if provided.
611    pub fn path(&self) -> Option<PathBuf> {
612        match self {
613            Self::Path(path) => Some(path.to_path_buf()),
614            Self::ContractInfo(info) => info.path.as_ref().map(PathBuf::from),
615        }
616    }
617
618    /// Returns the contract name if provided.
619    pub fn name(&self) -> Option<&str> {
620        match self {
621            Self::Path(_) => None,
622            Self::ContractInfo(info) => Some(&info.name),
623        }
624    }
625}
626
627impl FromStr for PathOrContractInfo {
628    type Err = eyre::Error;
629
630    fn from_str(s: &str) -> Result<Self> {
631        if let Ok(contract) = CompilerContractInfo::from_str(s) {
632            return Ok(Self::ContractInfo(contract));
633        }
634        let path = PathBuf::from(s);
635        if path.extension().is_some_and(|ext| ext == "sol" || ext == "vy") {
636            return Ok(Self::Path(path));
637        }
638        Err(eyre::eyre!("Invalid contract identifier, file is not *.sol or *.vy: {}", s))
639    }
640}
641
642impl std::fmt::Debug for PathOrContractInfo {
643    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
644        match self {
645            Self::Path(path) => write!(f, "Path({})", path.display()),
646            Self::ContractInfo(info) => {
647                write!(f, "ContractInfo({info})")
648            }
649        }
650    }
651}
652
653#[cfg(test)]
654mod tests {
655    use super::*;
656
657    #[test]
658    fn parse_contract_identifiers() {
659        let t = ["src/Counter.sol", "src/Counter.sol:Counter", "Counter"];
660
661        let i1 = PathOrContractInfo::from_str(t[0]).unwrap();
662        assert_eq!(i1, PathOrContractInfo::Path(PathBuf::from(t[0])));
663
664        let i2 = PathOrContractInfo::from_str(t[1]).unwrap();
665        assert_eq!(
666            i2,
667            PathOrContractInfo::ContractInfo(CompilerContractInfo {
668                path: Some("src/Counter.sol".to_string()),
669                name: "Counter".to_string()
670            })
671        );
672
673        let i3 = PathOrContractInfo::from_str(t[2]).unwrap();
674        assert_eq!(
675            i3,
676            PathOrContractInfo::ContractInfo(CompilerContractInfo {
677                path: None,
678                name: "Counter".to_string()
679            })
680        );
681    }
682}