foundry_common/
compile.rs

1//! Support for compiling [foundry_compilers::Project]
2
3use 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/// 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    #[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        // TODO: Avoid using std::process::exit(0).
162        // Replacing this with a return (e.g., Ok(ProjectCompileOutput::default())) would be more
163        // idiomatic, but it currently requires a `Default` bound on `C::Language`, which
164        // breaks compatibility with downstream crates like `foundry-cli`. This would need a
165        // broader refactor across the call chain. Leaving it as-is for now until a larger
166        // refactor is feasible.
167        if !project.paths.has_input_files() && self.files.is_empty() {
168            sh_println!("Nothing to compile")?;
169            std::process::exit(0);
170        }
171
172        // Taking is fine since we don't need these in `compile_with`.
173        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    /// Compiles the project with the given closure
192    ///
193    /// # Example
194    ///
195    /// ```ignore
196    /// use foundry_common::compile::ProjectCompiler;
197    /// let config = foundry_config::Config::load().unwrap();
198    /// let prj = config.project().unwrap();
199    /// ProjectCompiler::new().compile_with(|| Ok(prj.compile()?)).unwrap();
200    /// ```
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(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        // Filters out dev contracts (Test or Script)
446        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
481/// Returns the deployed or init size of the contract.
482fn 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            // we don't need to account for placeholders here, because library placeholders take up
493            // 40 characters: `__$<library hash>$__` which is the same as a 20byte address in hex.
494            let mut size = unlinked.len();
495            if unlinked.starts_with("0x") {
496                size -= 2;
497            }
498            // hex -> bytes
499            size / 2
500        }
501    };
502
503    Some(size)
504}
505
506/// How big the contract is and whether it is a dev contract where size limits can be neglected
507#[derive(Clone, Copy, Debug)]
508pub struct ContractInfo {
509    /// Size of the runtime code in bytes
510    pub runtime_size: usize,
511    /// Size of the initcode in bytes
512    pub init_size: usize,
513    /// A development contract is either a Script or a Test contract.
514    pub is_dev_contract: bool,
515}
516
517/// Compiles target file path.
518///
519/// If `quiet` no solc related output will be emitted to stdout.
520///
521/// If `verify` and it's a standalone script, throw error. Only allowed for projects.
522///
523/// **Note:** this expects the `target_path` to be absolute
524pub 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
535/// Creates a [Project] from an Etherscan source.
536pub 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    // make remappings absolute with our root
547    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    // add missing remappings
553    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    // root/
563    //   ContractName/
564    //     [source code]
565    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
586/// Configures the reporter and runs the given closure.
587pub 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/// Container type for parsing contract identifiers from CLI.
603///
604/// Passed string can be of the following forms:
605/// - `src/Counter.sol` - path to the contract file, in the case where it only contains one contract
606/// - `src/Counter.sol:Counter` - path to the contract file and the contract name
607/// - `Counter` - contract name only
608#[derive(Clone, PartialEq, Eq)]
609pub enum PathOrContractInfo {
610    /// Non-canoncalized path provided via CLI.
611    Path(PathBuf),
612    /// Contract info provided via CLI.
613    ContractInfo(CompilerContractInfo),
614}
615
616impl PathOrContractInfo {
617    /// Returns the path to the contract file if provided.
618    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    /// Returns the contract name if provided.
626    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}