Skip to main content

foundry_common/
compile.rs

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