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