Skip to main content

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