foundry_common/
compile.rs

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