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