Skip to main content

foundry_common/
compile.rs

1//! Support for compiling [foundry_compilers::Project]
2
3use crate::{
4    TestFunctionExt,
5    external_compiler::{ExternalCompilation, is_builtin_compiler_source},
6    fs::canonicalize_path,
7    preprocessor::DynamicTestLinkingPreprocessor,
8    shell,
9    term::SpinnerReporter,
10};
11use alloy_json_abi::JsonAbi;
12use comfy_table::{
13    Cell, Color, Table,
14    presets::{ASCII_FULL, ASCII_MARKDOWN},
15};
16use eyre::{OptionExt, Result};
17use foundry_block_explorers::contract::Metadata;
18use foundry_compilers::{
19    Artifact, Project, ProjectBuilder, ProjectCompileOutput, ProjectPathsConfig, SolcConfig,
20    artifacts::{
21        BytecodeObject, Contract, Severity, Source, output_selection::OutputSelection,
22        remappings::Remapping,
23    },
24    compilers::{
25        CompilationError, Compiler,
26        solc::{Solc, SolcCompiler},
27    },
28    info::ContractInfo as CompilerContractInfo,
29    multi::{MultiCompiler, MultiCompilerSettings},
30    project::Preprocessor,
31    report::{BasicStdoutReporter, NoReporter, Report},
32    solc::SolcSettings,
33};
34use foundry_config::Config;
35use num_format::{Locale, ToFormattedString};
36use revm::primitives::{eip170, eip3860, hardfork::SpecId};
37use solar::{
38    ast::{Arena, ContractKind, ItemKind},
39    interface::{Session, source_map::FileName},
40    parse::Parser,
41};
42use std::{
43    collections::{BTreeMap, BTreeSet, HashMap, HashSet},
44    fmt::Display,
45    io::IsTerminal,
46    path::{Component, Path, PathBuf},
47    str::FromStr,
48    sync::Arc,
49    time::Instant,
50};
51
52/// A Solar compiler instance, to grant syntactic and semantic analysis capabilities.
53pub type Analysis = Arc<solar::sema::Compiler>;
54
55/// Builder type to configure how to compile a project.
56///
57/// This is merely a wrapper for [`Project::compile()`] which also prints to stdout depending on its
58/// settings.
59#[must_use = "ProjectCompiler does nothing unless you call a `compile*` method"]
60pub struct ProjectCompiler {
61    /// The root of the project.
62    project_root: PathBuf,
63
64    /// Whether to also print contract names.
65    print_names: Option<bool>,
66
67    /// Whether to also print contract sizes.
68    print_sizes: Option<bool>,
69
70    /// Whether to print anything at all. Overrides other `print` options.
71    quiet: Option<bool>,
72
73    /// Whether to print the resolved settings for each compiler invocation.
74    print_compiler_settings: bool,
75
76    /// Whether to bail on compiler errors.
77    bail: Option<bool>,
78
79    /// Whether to ignore the contract initcode size limit introduced by EIP-3860.
80    ignore_eip_3860: bool,
81
82    /// Contract size limits used when reporting compiled contract sizes.
83    size_limits: ContractSizeLimits,
84
85    /// Extra files to include, that are not necessarily in the project's source directory.
86    files: Vec<PathBuf>,
87
88    /// Complete source roots to retry if filtering changed Solidity's inheritance ordering.
89    source_order_fallback: Vec<PathBuf>,
90
91    /// Paths used by external adapters to select compiler-native build units.
92    selected_paths: Vec<PathBuf>,
93
94    /// Whether to compile with dynamic linking tests and scripts.
95    dynamic_test_linking: bool,
96
97    /// Whether ABI acquisition may consult the compiler-owned ABI cache.
98    abi_cache: bool,
99
100    /// External compiler configuration.
101    external_compilers: Option<Config>,
102
103    /// Preserves the caller's artifact policy when ABI caching enables artifacts on a cloned
104    /// project.
105    external_writes: bool,
106}
107
108impl Default for ProjectCompiler {
109    #[inline]
110    fn default() -> Self {
111        Self::new()
112    }
113}
114
115impl ProjectCompiler {
116    /// Create a new builder with the default settings.
117    #[inline]
118    pub fn new() -> Self {
119        Self {
120            project_root: PathBuf::new(),
121            print_names: None,
122            print_sizes: None,
123            quiet: Some(crate::shell::is_quiet()),
124            print_compiler_settings: false,
125            bail: None,
126            ignore_eip_3860: false,
127            size_limits: ContractSizeLimits::default(),
128            files: Vec::new(),
129            source_order_fallback: Vec::new(),
130            selected_paths: Vec::new(),
131            dynamic_test_linking: false,
132            abi_cache: false,
133            external_compilers: None,
134            external_writes: true,
135        }
136    }
137
138    /// Sets whether to print contract names.
139    #[inline]
140    pub const fn print_names(mut self, yes: bool) -> Self {
141        self.print_names = Some(yes);
142        self
143    }
144
145    /// Sets whether to print contract sizes.
146    #[inline]
147    pub const fn print_sizes(mut self, yes: bool) -> Self {
148        self.print_sizes = Some(yes);
149        self
150    }
151
152    /// Sets whether to print anything at all. Overrides other `print` options.
153    #[inline]
154    #[doc(alias = "silent")]
155    pub const fn quiet(mut self, yes: bool) -> Self {
156        self.quiet = Some(yes);
157        self
158    }
159
160    /// Sets whether to print the resolved settings for each compiler invocation.
161    #[inline]
162    pub const fn print_compiler_settings(mut self, yes: bool) -> Self {
163        self.print_compiler_settings = yes;
164        self
165    }
166
167    /// Sets whether to bail on compiler errors.
168    #[inline]
169    pub const fn bail(mut self, yes: bool) -> Self {
170        self.bail = Some(yes);
171        self
172    }
173
174    /// Sets whether to ignore EIP-3860 initcode size limits.
175    #[inline]
176    pub const fn ignore_eip_3860(mut self, yes: bool) -> Self {
177        self.ignore_eip_3860 = yes;
178        self
179    }
180
181    /// Sets the contract size limits for size reports.
182    #[inline]
183    pub const fn size_limits(mut self, limits: ContractSizeLimits) -> Self {
184        self.size_limits = limits;
185        self
186    }
187
188    /// Sets extra files to include, that are not necessarily in the project's source dir.
189    #[inline]
190    pub fn files(mut self, files: impl IntoIterator<Item = PathBuf>) -> Self {
191        self.files.extend(files);
192        self
193    }
194
195    /// Sets paths used by external adapters to select compiler-native build units.
196    pub fn selected_paths(mut self, paths: impl IntoIterator<Item = PathBuf>) -> Self {
197        self.selected_paths.extend(paths);
198        self
199    }
200
201    /// Selects targets for external adapters and built-in sources where applicable.
202    pub fn target_files(mut self, paths: impl IntoIterator<Item = PathBuf>) -> Self {
203        for path in paths {
204            if is_builtin_compiler_source(&path) {
205                self.files.push(path.clone());
206            }
207            self.selected_paths.push(path);
208        }
209        self
210    }
211
212    /// Sets if tests should be dynamically linked.
213    #[inline]
214    pub const fn dynamic_test_linking(mut self, preprocess: bool) -> Self {
215        self.dynamic_test_linking = preprocess;
216        self
217    }
218
219    /// Enables explicitly configured external compilers for this build.
220    pub fn external_compilers(mut self, config: &Config) -> Self {
221        self.external_compilers = Some(config.clone());
222        self
223    }
224
225    /// Controls whether external artifacts and their cache may be published.
226    pub const fn external_artifacts(mut self, write: bool) -> Self {
227        self.external_writes = write;
228        self
229    }
230
231    /// Reuses normal artifacts before consulting the compiler-owned ABI cache.
232    ///
233    /// The project must request ABI output and may include other contract outputs.
234    /// The project's artifact policy controls writes to the secondary cache.
235    pub const fn cache_abi(mut self) -> Self {
236        self.abi_cache = true;
237        self
238    }
239
240    /// Retries once with these roots if compilation fails because a base contract is defined
241    /// after its derived contract. Removing roots can change traversal order in import cycles.
242    pub fn source_order_fallback(mut self, files: impl IntoIterator<Item = PathBuf>) -> Self {
243        self.source_order_fallback.extend(files);
244        self
245    }
246
247    /// Compiles the project.
248    #[instrument(target = "forge::compile", skip_all)]
249    pub fn compile<C: Compiler<CompilerContract = Contract>>(
250        mut self,
251        project: &Project<C>,
252    ) -> Result<ProjectCompileOutput<C>>
253    where
254        DynamicTestLinkingPreprocessor: Preprocessor<C>,
255    {
256        self.project_root = project.root().to_path_buf();
257
258        // TODO: Avoid using std::process::exit(0).
259        // Replacing this with a return (e.g., Ok(ProjectCompileOutput::default())) would be more
260        // idiomatic, but it currently requires a `Default` bound on `C::Language`, which
261        // breaks compatibility with downstream crates like `foundry-cli`. This would need a
262        // broader refactor across the call chain. Leaving it as-is for now until a larger
263        // refactor is feasible.
264        if !project.paths.has_input_files()
265            && self.files.is_empty()
266            && self.selected_paths.is_empty()
267            && self
268                .external_compilers
269                .as_ref()
270                .is_none_or(|config| config.external_compilers.is_empty())
271        {
272            sh_println!("Nothing to compile")?;
273            std::process::exit(0);
274        }
275
276        // Taking is fine since we don't need these in `compile_with`.
277        let files = std::mem::take(&mut self.files);
278        let fallback_files = std::mem::take(&mut self.source_order_fallback);
279        let explicit_selection = !files.is_empty() || !self.selected_paths.is_empty();
280        let selected_paths = std::mem::take(&mut self.selected_paths);
281        let preprocess = self.dynamic_test_linking;
282        let abi_cache = self.abi_cache;
283        let external_compilers = self.external_compilers.take();
284        let external_writes = self.external_writes && !project.no_artifacts;
285        self.compile_with(|| {
286            let external = external_compilers
287                .as_ref()
288                .map(|config| {
289                    ExternalCompilation::compile(config, &selected_paths, external_writes)
290                })
291                .transpose()?;
292            let sources = if explicit_selection {
293                Source::read_all(files)?
294            } else {
295                project.paths.read_input_files()?
296            };
297
298            let compile = |sources| {
299                let mut compiler =
300                    foundry_compilers::project::ProjectCompiler::with_sources(project, sources)?;
301                if preprocess {
302                    compiler = compiler.with_preprocessor(DynamicTestLinkingPreprocessor);
303                }
304                if abi_cache { compiler.compile_abi_cached() } else { compiler.compile() }
305            };
306            let mut output = compile(sources)?;
307            // Solc error 2449 can be introduced by filtering compilation roots in an import
308            // cycle. Retry before reporting diagnostics or bailing on the filtered output.
309            if !fallback_files.is_empty()
310                && output.output().errors.iter().any(|error| error.error_code() == Some(2449))
311            {
312                output = compile(Source::read_all(fallback_files)?)?;
313            }
314            if !output.has_compiler_errors()
315                && let Some(external) = external
316            {
317                external.merge(&mut output)?;
318            }
319            Ok(output)
320        })
321    }
322
323    /// Compiles the project with the given closure
324    fn compile_with<C: Compiler<CompilerContract = Contract>, F>(
325        self,
326        f: F,
327    ) -> Result<ProjectCompileOutput<C>>
328    where
329        F: FnOnce() -> Result<ProjectCompileOutput<C>>,
330    {
331        let quiet = self.quiet.unwrap_or(false);
332        let bail = self.bail.unwrap_or(true);
333
334        let output = with_compilation_reporter_and_settings(
335            quiet,
336            Some(self.project_root.clone()),
337            self.print_compiler_settings,
338            || {
339                tracing::debug!("compiling project");
340
341                let timer = Instant::now();
342                let r = f();
343                let elapsed = timer.elapsed();
344
345                tracing::debug!("finished compiling in {:.3}s", elapsed.as_secs_f64());
346                r
347            },
348        )?;
349
350        if bail && output.has_compiler_errors() {
351            eyre::bail!("{output}");
352        }
353
354        if !quiet && !shell::is_json() {
355            if output.is_unchanged() {
356                sh_println!("No files changed, compilation skipped")?;
357            } else {
358                // print the compiler output / warnings
359                sh_println!("{output}")?;
360            }
361        }
362
363        // Quiet mode suppresses reports, but size limits still apply.
364        if !(shell::is_json() && output.has_compiler_errors()) {
365            self.handle_output(&output)?;
366        }
367
368        Ok(output)
369    }
370
371    /// Prints requested reports and checks contract size limits.
372    fn handle_output<C: Compiler<CompilerContract = Contract>>(
373        &self,
374        output: &ProjectCompileOutput<C>,
375    ) -> Result<()> {
376        let quiet = self.quiet.unwrap_or(false);
377        let print_names = self.print_names.unwrap_or(false) && !quiet;
378        let print_sizes = self.print_sizes.unwrap_or(false);
379
380        // print any sizes or names
381        if print_names {
382            let mut artifacts: BTreeMap<_, Vec<_>> = BTreeMap::new();
383            for (name, (_, version)) in output.versioned_artifacts() {
384                artifacts.entry(version).or_default().push(name);
385            }
386
387            if shell::is_json() {
388                sh_println!("{}", serde_json::to_string(&artifacts).unwrap())?;
389            } else {
390                for (version, names) in artifacts {
391                    sh_println!(
392                        "  compiler version: {}.{}.{}",
393                        version.major,
394                        version.minor,
395                        version.patch
396                    )?;
397                    for name in names {
398                        sh_println!("    - {name}")?;
399                    }
400                }
401            }
402        }
403
404        if print_sizes {
405            // add extra newline if names were already printed
406            if print_names && !shell::is_json() {
407                sh_println!()?;
408            }
409
410            let mut size_report =
411                SizeReport { contracts: BTreeMap::new(), limits: self.size_limits };
412
413            let mut artifacts: BTreeMap<String, Vec<_>> = BTreeMap::new();
414            for (id, artifact) in output.artifact_ids().filter(|(id, _)| {
415                // filter out forge-std specific contracts
416                !id.source.to_string_lossy().contains("/forge-std/src/")
417            }) {
418                artifacts.entry(id.name.clone()).or_default().push((id.source, artifact));
419            }
420
421            // Internal libraries are inlined into consumers and never deployed; skip them.
422            // Only artifacts whose ABI has no functions can be internal libraries, so restrict the
423            // solar parse to those sources to avoid a second full parse pass.
424            let abs_source = |path: &Path| -> PathBuf {
425                if path.is_absolute() { path.to_path_buf() } else { self.project_root.join(path) }
426            };
427            let source_paths = artifacts
428                .values()
429                .flatten()
430                .filter(|(_, artifact)| {
431                    artifact.abi.as_ref().is_some_and(|abi| abi.functions().next().is_none())
432                })
433                .map(|(path, _)| abs_source(path))
434                .collect::<BTreeSet<_>>();
435            let libraries = collect_libraries(&source_paths);
436
437            for (name, artifact_list) in artifacts {
438                // A library with no functions in its ABI is internal-only; fail open if the ABI is
439                // missing. Filter first so the duplicate-name suffix below reflects kept contracts.
440                let kept = artifact_list
441                    .iter()
442                    .filter(|(path, artifact)| {
443                        let is_library = libraries
444                            .get(&abs_source(path))
445                            .is_some_and(|libs| libs.contains(&name));
446                        let has_no_abi_functions = artifact
447                            .abi
448                            .as_ref()
449                            .is_some_and(|abi| abi.functions().next().is_none());
450                        !(is_library && has_no_abi_functions)
451                    })
452                    .collect::<Vec<_>>();
453
454                for (path, artifact) in &kept {
455                    let runtime_size = contract_size(*artifact, false).unwrap_or_default();
456                    let init_size = contract_size(*artifact, true).unwrap_or_default();
457
458                    let is_dev_contract = artifact.abi.as_ref().is_some_and(|abi| {
459                        abi.functions().any(|f| {
460                            f.test_function_kind().is_known()
461                                || matches!(f.name.as_str(), "IS_TEST" | "IS_SCRIPT")
462                        })
463                    });
464
465                    let unique_name = if kept.len() > 1 {
466                        format!(
467                            "{} ({})",
468                            name,
469                            path.strip_prefix(&self.project_root).unwrap_or(path).display()
470                        )
471                    } else {
472                        name.clone()
473                    };
474
475                    size_report.contracts.insert(
476                        unique_name,
477                        ContractInfo { runtime_size, init_size, is_dev_contract },
478                    );
479                }
480            }
481
482            if !quiet {
483                sh_println!("{size_report}")?;
484            }
485
486            let runtime_eip = match size_report.limits.runtime {
487                CONTRACT_RUNTIME_SIZE_LIMIT => "EIP-170: ",
488                AMSTERDAM_CONTRACT_RUNTIME_SIZE_LIMIT => "EIP-7954: ",
489                _ => "",
490            };
491            eyre::ensure!(
492                !size_report.exceeds_runtime_size_limit(),
493                "some contracts exceed the runtime size limit ({runtime_eip}{} bytes)",
494                size_report.limits.runtime
495            );
496            // Check size limits only if not ignoring EIP-3860
497            let initcode_eip = match size_report.limits.initcode {
498                CONTRACT_INITCODE_SIZE_LIMIT => "EIP-3860: ",
499                AMSTERDAM_CONTRACT_INITCODE_SIZE_LIMIT => "EIP-7954: ",
500                _ => "",
501            };
502            eyre::ensure!(
503                self.ignore_eip_3860 || !size_report.exceeds_initcode_size_limit(),
504                "some contracts exceed the initcode size limit ({initcode_eip}{} bytes)",
505                size_report.limits.initcode
506            );
507        }
508
509        Ok(())
510    }
511}
512
513// https://eips.ethereum.org/EIPS/eip-170
514const CONTRACT_RUNTIME_SIZE_LIMIT: usize = eip170::MAX_CODE_SIZE;
515
516// https://eips.ethereum.org/EIPS/eip-3860
517const CONTRACT_INITCODE_SIZE_LIMIT: usize = eip3860::MAX_INITCODE_SIZE;
518
519// https://eips.ethereum.org/EIPS/eip-7954
520const AMSTERDAM_CONTRACT_RUNTIME_SIZE_LIMIT: usize = 65_536;
521const AMSTERDAM_CONTRACT_INITCODE_SIZE_LIMIT: usize = 131_072;
522
523const CONTRACT_RUNTIME_SIZE_WARN_THRESHOLD: usize = 18_000;
524const CONTRACT_INITCODE_SIZE_WARN_THRESHOLD: usize = 36_000;
525
526/// Runtime and initcode byte-size limits for compiled contract size reports.
527#[derive(Clone, Copy, Debug, PartialEq, Eq)]
528pub struct ContractSizeLimits {
529    /// Maximum deployed runtime bytecode size.
530    pub runtime: usize,
531    /// Maximum initcode bytecode size.
532    pub initcode: usize,
533}
534
535impl ContractSizeLimits {
536    /// Creates a new set of contract size limits.
537    pub const fn new(runtime: usize, initcode: usize) -> Self {
538        Self { runtime, initcode }
539    }
540
541    /// Creates limits from a runtime code-size limit, using the EIP-3860 2x initcode ratio.
542    pub const fn with_runtime_limit(runtime: usize) -> Self {
543        Self { runtime, initcode: runtime.saturating_mul(2) }
544    }
545
546    /// Returns the protocol limits active for an EVM specification.
547    pub const fn for_spec_id(spec_id: SpecId) -> Self {
548        if spec_id.is_enabled_in(SpecId::AMSTERDAM) {
549            Self::new(AMSTERDAM_CONTRACT_RUNTIME_SIZE_LIMIT, AMSTERDAM_CONTRACT_INITCODE_SIZE_LIMIT)
550        } else {
551            Self::new(CONTRACT_RUNTIME_SIZE_LIMIT, CONTRACT_INITCODE_SIZE_LIMIT)
552        }
553    }
554
555    const fn runtime_warning_threshold(self) -> usize {
556        scaled_threshold(
557            self.runtime,
558            CONTRACT_RUNTIME_SIZE_WARN_THRESHOLD,
559            CONTRACT_RUNTIME_SIZE_LIMIT,
560        )
561    }
562
563    const fn initcode_warning_threshold(self) -> usize {
564        scaled_threshold(
565            self.initcode,
566            CONTRACT_INITCODE_SIZE_WARN_THRESHOLD,
567            CONTRACT_INITCODE_SIZE_LIMIT,
568        )
569    }
570}
571
572impl Default for ContractSizeLimits {
573    fn default() -> Self {
574        Self::new(CONTRACT_RUNTIME_SIZE_LIMIT, CONTRACT_INITCODE_SIZE_LIMIT)
575    }
576}
577
578const fn scaled_threshold(limit: usize, threshold: usize, default_limit: usize) -> usize {
579    limit.saturating_mul(threshold) / default_limit
580}
581
582/// Contracts with info about their size
583pub struct SizeReport {
584    /// `contract name -> info`
585    pub contracts: BTreeMap<String, ContractInfo>,
586    /// Size limits used to calculate margins and failures.
587    pub limits: ContractSizeLimits,
588}
589
590impl SizeReport {
591    /// Returns the maximum runtime code size, excluding dev contracts.
592    pub fn max_runtime_size(&self) -> usize {
593        self.contracts
594            .values()
595            .filter(|c| !c.is_dev_contract)
596            .map(|c| c.runtime_size)
597            .max()
598            .unwrap_or(0)
599    }
600
601    /// Returns the maximum initcode size, excluding dev contracts.
602    pub fn max_init_size(&self) -> usize {
603        self.contracts
604            .values()
605            .filter(|c| !c.is_dev_contract)
606            .map(|c| c.init_size)
607            .max()
608            .unwrap_or(0)
609    }
610
611    /// Returns true if any contract exceeds the runtime size limit, excluding dev contracts.
612    pub fn exceeds_runtime_size_limit(&self) -> bool {
613        self.max_runtime_size() > self.limits.runtime
614    }
615
616    /// Returns true if any contract exceeds the initcode size limit, excluding dev contracts.
617    pub fn exceeds_initcode_size_limit(&self) -> bool {
618        self.max_init_size() > self.limits.initcode
619    }
620}
621
622impl Display for SizeReport {
623    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
624        if shell::is_json() {
625            writeln!(f, "{}", self.format_json_output())?;
626        } else {
627            writeln!(f, "\n{}", self.format_table_output())?;
628        }
629        Ok(())
630    }
631}
632
633impl SizeReport {
634    fn format_json_output(&self) -> String {
635        let contracts = self
636            .contracts
637            .iter()
638            .filter(|(_, c)| !c.is_dev_contract && (c.runtime_size > 0 || c.init_size > 0))
639            .map(|(name, contract)| {
640                (
641                    name.clone(),
642                    serde_json::json!({
643                        "runtime_size": contract.runtime_size,
644                        "init_size": contract.init_size,
645                        "runtime_margin": self.limits.runtime as isize - contract.runtime_size as isize,
646                        "init_margin": self.limits.initcode as isize - contract.init_size as isize,
647                    }),
648                )
649            })
650            .collect::<serde_json::Map<_, _>>();
651
652        serde_json::to_string(&contracts).unwrap()
653    }
654
655    fn format_table_output(&self) -> Table {
656        let mut table = Table::new();
657        if shell::is_markdown() {
658            table.load_style(ASCII_MARKDOWN);
659        } else {
660            table.load_style(ASCII_FULL.with_rounded_corners());
661        }
662
663        table.set_header(vec![
664            Cell::new("Contract"),
665            Cell::new("Runtime Size (B)"),
666            Cell::new("Initcode Size (B)"),
667            Cell::new("Runtime Margin (B)"),
668            Cell::new("Initcode Margin (B)"),
669        ]);
670
671        // Filters out dev contracts (Test or Script)
672        let contracts = self
673            .contracts
674            .iter()
675            .filter(|(_, c)| !c.is_dev_contract && (c.runtime_size > 0 || c.init_size > 0));
676        let runtime_warning_threshold = self.limits.runtime_warning_threshold();
677        let initcode_warning_threshold = self.limits.initcode_warning_threshold();
678        for (name, contract) in contracts {
679            let runtime_margin = self.limits.runtime as isize - contract.runtime_size as isize;
680            let init_margin = self.limits.initcode as isize - contract.init_size as isize;
681
682            let runtime_color = if contract.runtime_size < runtime_warning_threshold {
683                Color::Reset
684            } else if contract.runtime_size <= self.limits.runtime {
685                Color::Yellow
686            } else {
687                Color::Red
688            };
689
690            let init_color = if contract.init_size < initcode_warning_threshold {
691                Color::Reset
692            } else if contract.init_size <= self.limits.initcode {
693                Color::Yellow
694            } else {
695                Color::Red
696            };
697
698            let locale = &Locale::en;
699            table.add_row([
700                Cell::new(name),
701                Cell::new(contract.runtime_size.to_formatted_string(locale)).fg(runtime_color),
702                Cell::new(contract.init_size.to_formatted_string(locale)).fg(init_color),
703                Cell::new(runtime_margin.to_formatted_string(locale)).fg(runtime_color),
704                Cell::new(init_margin.to_formatted_string(locale)).fg(init_color),
705            ]);
706        }
707
708        table
709    }
710}
711
712/// Parses each source file with solar and returns the library names declared in it.
713///
714/// Files that fail to parse are skipped, so a missing entry means "unknown", not "no libraries".
715fn collect_libraries(sources: &BTreeSet<PathBuf>) -> HashMap<PathBuf, HashSet<String>> {
716    let mut result: HashMap<PathBuf, HashSet<String>> = HashMap::new();
717    let sess = Session::builder().with_silent_emitter(None).build();
718    let _ = sess.enter(|| -> solar::interface::Result<()> {
719        for path in sources {
720            let arena = Arena::new();
721            let mut parser = match Parser::from_lazy_source_code(
722                &sess,
723                &arena,
724                FileName::from(path.clone()),
725                || std::fs::read_to_string(path),
726            ) {
727                Ok(parser) => parser,
728                Err(_) => continue,
729            };
730            let Ok(ast) = parser.parse_file() else { continue };
731            let libs = ast
732                .items
733                .iter()
734                .filter_map(|item| match &item.kind {
735                    ItemKind::Contract(c) if c.kind == ContractKind::Library => {
736                        Some(c.name.as_str().to_string())
737                    }
738                    _ => None,
739                })
740                .collect::<HashSet<_>>();
741            if !libs.is_empty() {
742                result.insert(path.clone(), libs);
743            }
744        }
745        Ok(())
746    });
747    result
748}
749
750/// Returns the deployed or init size of the contract.
751fn contract_size<T: Artifact>(artifact: &T, initcode: bool) -> Option<usize> {
752    let bytecode = if initcode {
753        artifact.get_bytecode_object()?
754    } else {
755        artifact.get_deployed_bytecode_object()?
756    };
757
758    let size = match bytecode.as_ref() {
759        BytecodeObject::Bytecode(bytes) => bytes.len(),
760        BytecodeObject::Unlinked(unlinked) => {
761            // we don't need to account for placeholders here, because library placeholders take up
762            // 40 characters: `__$<library hash>$__` which is the same as a 20byte address in hex.
763            let mut size = unlinked.len();
764            if unlinked.starts_with("0x") {
765                size -= 2;
766            }
767            // hex -> bytes
768            size / 2
769        }
770    };
771
772    Some(size)
773}
774
775/// How big the contract is and whether it is a dev contract where size limits can be neglected
776#[derive(Clone, Copy, Debug)]
777pub struct ContractInfo {
778    /// Size of the runtime code in bytes
779    pub runtime_size: usize,
780    /// Size of the initcode in bytes
781    pub init_size: usize,
782    /// A development contract is either a Script or a Test contract.
783    pub is_dev_contract: bool,
784}
785
786/// Compiles target file path.
787///
788/// If `quiet` is set, the compilation reporter's progress/status output is suppressed.
789/// (When not suppressed, that output is emitted to stderr; see `with_compilation_reporter`.)
790///
791/// **Note:** this expects the `target_path` to be absolute
792pub fn compile_target<C: Compiler<CompilerContract = Contract>>(
793    target_path: &Path,
794    project: &Project<C>,
795    quiet: bool,
796) -> Result<ProjectCompileOutput<C>>
797where
798    DynamicTestLinkingPreprocessor: Preprocessor<C>,
799{
800    ProjectCompiler::new().quiet(quiet).files([target_path.into()]).compile(project)
801}
802
803/// Compiles the project requesting only ABI output.
804pub fn compile_abi_project<C: Compiler<CompilerContract = Contract>>(
805    project: &mut Project<C>,
806    mut compiler: ProjectCompiler,
807) -> Result<ProjectCompileOutput<C>>
808where
809    DynamicTestLinkingPreprocessor: Preprocessor<C>,
810{
811    project.update_output_selection(|selection| {
812        // Request ABI so compilers populate `contracts` without producing bytecode outputs.
813        *selection = OutputSelection::common_output_selection(["abi".to_string()]);
814    });
815    // Cached ABI artifacts do not retain compiler diagnostics.
816    compiler.abi_cache = (compiler.abi_cache || project.no_artifacts)
817        && project.compiler_severity_filter == Severity::Error;
818    compiler.compile(project)
819}
820
821/// Acquires ABI output with compiler-owned persistence separate from normal artifacts.
822///
823/// Requests for additional files or full build info retain their existing output behavior.
824pub fn compile_abi_project_cached<C: Compiler<CompilerContract = Contract>>(
825    project: &mut Project<C>,
826    mut compiler: ProjectCompiler,
827) -> Result<ProjectCompileOutput<C>>
828where
829    DynamicTestLinkingPreprocessor: Preprocessor<C>,
830{
831    if !project.cached
832        || project.build_info
833        || project.artifacts.additional_files != Default::default()
834        || project.compiler_severity_filter != Severity::Error
835    {
836        return compile_abi_project(project, compiler);
837    }
838    let mut cached_project = project.clone();
839    cached_project.no_artifacts = false;
840    compiler.abi_cache = true;
841    compiler.external_writes &= !project.no_artifacts;
842    compile_abi_project(&mut cached_project, compiler)
843}
844
845/// Compiles the target contract requesting only ABI output and returns its ABI.
846pub fn compile_target_abi(
847    project: &mut Project<MultiCompiler>,
848    target_path: &Path,
849    target_name: &str,
850) -> Result<JsonAbi> {
851    let target_path = canonicalize_path(target_path)?;
852    let output = compile_abi_project(
853        project,
854        ProjectCompiler::new().quiet(true).files([target_path.clone()]),
855    )?;
856
857    let artifact = output
858        .find(&target_path, target_name)
859        .ok_or_eyre("failed to find target artifact when compiling for abi")?;
860    artifact.abi.clone().ok_or_eyre("target artifact does not have an ABI")
861}
862
863/// Creates a [Project] from an Etherscan source.
864pub fn etherscan_project(metadata: &Metadata, target_path: &Path) -> Result<Project> {
865    let target_path = canonicalize_path(target_path)?;
866    let sources_path = target_path.join(&metadata.contract_name);
867    metadata.source_tree().write_to(&target_path)?;
868
869    let mut settings = metadata.settings()?;
870
871    // make remappings absolute with our root
872    //
873    // The remappings come from the explorer's copy of the contract's compiler settings, which is
874    // attacker controlled: a target of `../../..` would otherwise point the compiler at sources
875    // outside the checkout and let a verified contract pull arbitrary local files into the build.
876    for remapping in &mut settings.remappings {
877        let new_path = sources_path.join(sanitize_relative_path(remapping.path.as_ref()));
878        remapping.path = new_path.display().to_string();
879    }
880
881    // add missing remappings
882    if !settings.remappings.iter().any(|remapping| remapping.name.starts_with("@openzeppelin/")) {
883        let oz = Remapping {
884            context: None,
885            name: "@openzeppelin/".into(),
886            path: sources_path.join("@openzeppelin").display().to_string(),
887        };
888        settings.remappings.push(oz);
889    }
890
891    // root/
892    //   ContractName/
893    //     [source code]
894    let paths = ProjectPathsConfig::builder()
895        .sources(sources_path.clone())
896        .remappings(settings.remappings.clone())
897        .build_with_root(sources_path);
898
899    // TODO: detect vyper
900    let v = metadata.compiler_version()?;
901    let solc = Solc::find_or_install(&v)?;
902
903    let compiler = MultiCompiler { solc: Some(SolcCompiler::Specific(solc)), vyper: None };
904
905    Ok(ProjectBuilder::<MultiCompiler>::default()
906        .settings(MultiCompilerSettings {
907            solc: SolcSettings {
908                settings: SolcConfig::builder().settings(settings).build(),
909                ..Default::default()
910            },
911            ..Default::default()
912        })
913        .paths(paths)
914        .ephemeral()
915        .no_artifacts()
916        .build(compiler)?)
917}
918
919/// Normalizes an untrusted path relative to the source root, discarding leading separators,
920/// drive prefixes, and parent components that would escape that root.
921fn sanitize_relative_path(path: &Path) -> PathBuf {
922    let mut normalized = PathBuf::new();
923    for component in path.components() {
924        match component {
925            Component::Normal(part) => normalized.push(part),
926            Component::ParentDir => {
927                normalized.pop();
928            }
929            _ => {}
930        }
931    }
932    normalized
933}
934
935/// Adds `storageLayout` to the compiler output selection for the given project.
936pub fn add_storage_layout_output<C: Compiler<CompilerContract = Contract>>(
937    project: &mut Project<C>,
938) {
939    project.artifacts.additional_values.storage_layout = true;
940    project.update_output_selection(|selection| {
941        for contract_selection in selection.0.values_mut() {
942            for selection in contract_selection.values_mut() {
943                selection.push("storageLayout".to_string());
944            }
945        }
946    })
947}
948
949/// Configures the reporter and runs the given closure.
950///
951/// In TTY mode, [`SpinnerReporter`] paints the progress to stderr. The non-TTY fallback
952/// still writes to stdout via `BasicStdoutReporter`; migrating that path to stderr is
953/// part of the per-command stdout migration tracked in `docs/dev/output-channels.md`
954/// (it would shift many existing snapshot tests at once).
955pub fn with_compilation_reporter<O>(
956    quiet: bool,
957    project_root: Option<PathBuf>,
958    f: impl FnOnce() -> O,
959) -> O {
960    with_compilation_reporter_and_settings(quiet, project_root, false, f)
961}
962
963fn with_compilation_reporter_and_settings<O>(
964    quiet: bool,
965    project_root: Option<PathBuf>,
966    print_compiler_settings: bool,
967    f: impl FnOnce() -> O,
968) -> O {
969    #[expect(clippy::collapsible_else_if)]
970    let reporter = if quiet || shell::is_json() {
971        Report::new(NoReporter::default())
972    } else {
973        if std::io::stderr().is_terminal() {
974            Report::new(
975                SpinnerReporter::spawn(project_root)
976                    .with_compiler_settings(print_compiler_settings),
977            )
978        } else {
979            Report::new(
980                BasicStdoutReporter::default().with_compiler_settings(print_compiler_settings),
981            )
982        }
983    };
984
985    foundry_compilers::report::with_scoped(&reporter, f)
986}
987
988/// Container type for parsing contract identifiers from CLI.
989///
990/// Passed string can be of the following forms:
991/// - `src/Counter.sol` - path to the contract file, in the case where it only contains one contract
992/// - `src/Counter.sol:Counter` - path to the contract file and the contract name
993/// - `Counter` - contract name only
994#[derive(Clone, PartialEq, Eq)]
995pub enum PathOrContractInfo {
996    /// Non-canonicalized path provided via CLI.
997    Path(PathBuf),
998    /// Contract info provided via CLI.
999    ContractInfo(CompilerContractInfo),
1000}
1001
1002impl PathOrContractInfo {
1003    /// Returns the path to the contract file if provided.
1004    pub fn path(&self) -> Option<PathBuf> {
1005        match self {
1006            Self::Path(path) => Some(path.clone()),
1007            Self::ContractInfo(info) => info.path.as_ref().map(PathBuf::from),
1008        }
1009    }
1010
1011    /// Returns the contract name if provided.
1012    pub fn name(&self) -> Option<&str> {
1013        match self {
1014            Self::Path(_) => None,
1015            Self::ContractInfo(info) => Some(&info.name),
1016        }
1017    }
1018}
1019
1020impl FromStr for PathOrContractInfo {
1021    type Err = eyre::Error;
1022
1023    fn from_str(s: &str) -> Result<Self> {
1024        if let Ok(contract) = CompilerContractInfo::from_str(s) {
1025            return Ok(Self::ContractInfo(contract));
1026        }
1027        let path = PathBuf::from(s);
1028        if path.extension().is_some_and(|ext| ext == "sol" || ext == "vy") {
1029            return Ok(Self::Path(path));
1030        }
1031        Err(eyre::eyre!("Invalid contract identifier, file is not *.sol or *.vy: {}", s))
1032    }
1033}
1034
1035impl std::fmt::Debug for PathOrContractInfo {
1036    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1037        match self {
1038            Self::Path(path) => write!(f, "Path({})", path.display()),
1039            Self::ContractInfo(info) => {
1040                write!(f, "ContractInfo({info})")
1041            }
1042        }
1043    }
1044}
1045
1046#[cfg(test)]
1047mod tests {
1048    use super::*;
1049
1050    #[test]
1051    fn parse_contract_identifiers() {
1052        let t = ["src/Counter.sol", "src/Counter.sol:Counter", "Counter"];
1053
1054        let i1 = PathOrContractInfo::from_str(t[0]).unwrap();
1055        assert_eq!(i1, PathOrContractInfo::Path(PathBuf::from(t[0])));
1056
1057        let i2 = PathOrContractInfo::from_str(t[1]).unwrap();
1058        assert_eq!(
1059            i2,
1060            PathOrContractInfo::ContractInfo(CompilerContractInfo {
1061                path: Some("src/Counter.sol".to_string()),
1062                name: "Counter".to_string()
1063            })
1064        );
1065
1066        let i3 = PathOrContractInfo::from_str(t[2]).unwrap();
1067        assert_eq!(
1068            i3,
1069            PathOrContractInfo::ContractInfo(CompilerContractInfo {
1070                path: None,
1071                name: "Counter".to_string()
1072            })
1073        );
1074    }
1075
1076    #[test]
1077    fn size_report_uses_configured_limits() {
1078        let mut contracts = BTreeMap::new();
1079        contracts.insert(
1080            "LargeContract".to_string(),
1081            ContractInfo { runtime_size: 30_000, init_size: 60_000, is_dev_contract: false },
1082        );
1083
1084        let default_report =
1085            SizeReport { contracts: contracts.clone(), limits: ContractSizeLimits::default() };
1086        assert!(default_report.exceeds_runtime_size_limit());
1087        assert!(default_report.exceeds_initcode_size_limit());
1088
1089        let custom_report =
1090            SizeReport { contracts, limits: ContractSizeLimits::new(131_072, 262_144) };
1091        assert!(!custom_report.exceeds_runtime_size_limit());
1092        assert!(!custom_report.exceeds_initcode_size_limit());
1093        let output: serde_json::Value =
1094            serde_json::from_str(&custom_report.format_json_output()).unwrap();
1095        assert_eq!(
1096            output,
1097            serde_json::json!({
1098                "LargeContract": {
1099                    "runtime_size": 30000,
1100                    "init_size": 60000,
1101                    "runtime_margin": 101072,
1102                    "init_margin": 202144,
1103                }
1104            })
1105        );
1106    }
1107
1108    #[test]
1109    fn contract_size_limits_derive_initcode_limit_from_runtime_limit() {
1110        assert_eq!(
1111            ContractSizeLimits::with_runtime_limit(50_000),
1112            ContractSizeLimits::new(50_000, 100_000)
1113        );
1114    }
1115
1116    #[test]
1117    fn contract_size_limits_follow_evm_spec() {
1118        assert_eq!(ContractSizeLimits::for_spec_id(SpecId::OSAKA), ContractSizeLimits::default());
1119        assert_eq!(
1120            ContractSizeLimits::for_spec_id(SpecId::AMSTERDAM),
1121            ContractSizeLimits::new(65_536, 131_072)
1122        );
1123    }
1124
1125    #[test]
1126    fn sanitized_remapping_paths_stay_inside_the_root() {
1127        let root = Path::new("/tmp/sources");
1128        for path in ["../../../etc", "a/../../../etc", "/etc", "./a/../b"] {
1129            let joined = root.join(sanitize_relative_path(Path::new(path)));
1130            assert!(
1131                joined.starts_with(root) && !joined.components().any(|c| c == Component::ParentDir),
1132                "{path} escaped the root: {}",
1133                joined.display()
1134            );
1135        }
1136
1137        for (path, expected) in [("src/../lib/", "lib"), ("./a/../b", "b"), ("a/../../lib", "lib")]
1138        {
1139            assert_eq!(sanitize_relative_path(Path::new(path)), Path::new(expected));
1140        }
1141
1142        // Ordinary relative paths are left alone.
1143        assert_eq!(
1144            sanitize_relative_path(Path::new("lib/openzeppelin")),
1145            Path::new("lib/openzeppelin")
1146        );
1147    }
1148}