use crate::{
reports::{report_kind, ReportKind},
shell,
term::SpinnerReporter,
TestFunctionExt,
};
use comfy_table::{presets::ASCII_MARKDOWN, Attribute, Cell, CellAlignment, Color, Table};
use eyre::Result;
use foundry_block_explorers::contract::Metadata;
use foundry_compilers::{
artifacts::{remappings::Remapping, BytecodeObject, Source},
compilers::{
solc::{Solc, SolcCompiler},
Compiler,
},
report::{BasicStdoutReporter, NoReporter, Report},
solc::SolcSettings,
Artifact, Project, ProjectBuilder, ProjectCompileOutput, ProjectPathsConfig, SolcConfig,
};
use num_format::{Locale, ToFormattedString};
use std::{
collections::BTreeMap,
fmt::Display,
io::IsTerminal,
path::{Path, PathBuf},
time::Instant,
};
#[must_use = "ProjectCompiler does nothing unless you call a `compile*` method"]
pub struct ProjectCompiler {
verify: Option<bool>,
print_names: Option<bool>,
print_sizes: Option<bool>,
quiet: Option<bool>,
bail: Option<bool>,
ignore_eip_3860: bool,
files: Vec<PathBuf>,
}
impl Default for ProjectCompiler {
#[inline]
fn default() -> Self {
Self::new()
}
}
impl ProjectCompiler {
#[inline]
pub fn new() -> Self {
Self {
verify: None,
print_names: None,
print_sizes: None,
quiet: Some(crate::shell::is_quiet()),
bail: None,
ignore_eip_3860: false,
files: Vec::new(),
}
}
#[inline]
pub fn verify(mut self, yes: bool) -> Self {
self.verify = Some(yes);
self
}
#[inline]
pub fn print_names(mut self, yes: bool) -> Self {
self.print_names = Some(yes);
self
}
#[inline]
pub fn print_sizes(mut self, yes: bool) -> Self {
self.print_sizes = Some(yes);
self
}
#[inline]
#[doc(alias = "silent")]
pub fn quiet(mut self, yes: bool) -> Self {
self.quiet = Some(yes);
self
}
#[inline]
pub fn bail(mut self, yes: bool) -> Self {
self.bail = Some(yes);
self
}
#[inline]
pub fn ignore_eip_3860(mut self, yes: bool) -> Self {
self.ignore_eip_3860 = yes;
self
}
#[inline]
pub fn files(mut self, files: impl IntoIterator<Item = PathBuf>) -> Self {
self.files.extend(files);
self
}
pub fn compile<C: Compiler>(mut self, project: &Project<C>) -> Result<ProjectCompileOutput<C>> {
if !project.paths.has_input_files() && self.files.is_empty() {
sh_println!("Nothing to compile")?;
std::process::exit(0);
}
let files = std::mem::take(&mut self.files);
self.compile_with(|| {
let sources = if !files.is_empty() {
Source::read_all(files)?
} else {
project.paths.read_input_files()?
};
foundry_compilers::project::ProjectCompiler::with_sources(project, sources)?
.compile()
.map_err(Into::into)
})
}
#[instrument(target = "forge::compile", skip_all)]
fn compile_with<C: Compiler, F>(self, f: F) -> Result<ProjectCompileOutput<C>>
where
F: FnOnce() -> Result<ProjectCompileOutput<C>>,
{
let quiet = self.quiet.unwrap_or(false);
let bail = self.bail.unwrap_or(true);
let output = with_compilation_reporter(self.quiet.unwrap_or(false), || {
tracing::debug!("compiling project");
let timer = Instant::now();
let r = f();
let elapsed = timer.elapsed();
tracing::debug!("finished compiling in {:.3}s", elapsed.as_secs_f64());
r
})?;
if bail && output.has_compiler_errors() {
eyre::bail!("{output}")
}
if !quiet {
if !shell::is_json() {
if output.is_unchanged() {
sh_println!("No files changed, compilation skipped")?;
} else {
sh_println!("{output}")?;
}
}
self.handle_output(&output);
}
Ok(output)
}
fn handle_output<C: Compiler>(&self, output: &ProjectCompileOutput<C>) {
let print_names = self.print_names.unwrap_or(false);
let print_sizes = self.print_sizes.unwrap_or(false);
if print_names {
let mut artifacts: BTreeMap<_, Vec<_>> = BTreeMap::new();
for (name, (_, version)) in output.versioned_artifacts() {
artifacts.entry(version).or_default().push(name);
}
if shell::is_json() {
let _ = sh_println!("{}", serde_json::to_string(&artifacts).unwrap());
} else {
for (version, names) in artifacts {
let _ = sh_println!(
" compiler version: {}.{}.{}",
version.major,
version.minor,
version.patch
);
for name in names {
let _ = sh_println!(" - {name}");
}
}
}
}
if print_sizes {
if print_names && !shell::is_json() {
let _ = sh_println!();
}
let mut size_report =
SizeReport { report_kind: report_kind(), contracts: BTreeMap::new() };
let artifacts: BTreeMap<_, _> = output
.artifact_ids()
.filter(|(id, _)| {
!id.source.to_string_lossy().contains("/forge-std/src/")
})
.map(|(id, artifact)| (id.name, artifact))
.collect();
for (name, artifact) in artifacts {
let runtime_size = contract_size(artifact, false).unwrap_or_default();
let init_size = contract_size(artifact, true).unwrap_or_default();
let is_dev_contract = artifact
.abi
.as_ref()
.map(|abi| {
abi.functions().any(|f| {
f.test_function_kind().is_known() ||
matches!(f.name.as_str(), "IS_TEST" | "IS_SCRIPT")
})
})
.unwrap_or(false);
size_report
.contracts
.insert(name, ContractInfo { runtime_size, init_size, is_dev_contract });
}
let _ = sh_println!("{size_report}");
if size_report.exceeds_runtime_size_limit() {
std::process::exit(1);
}
if !self.ignore_eip_3860 && size_report.exceeds_initcode_size_limit() {
std::process::exit(1);
}
}
}
}
const CONTRACT_RUNTIME_SIZE_LIMIT: usize = 24576;
const CONTRACT_INITCODE_SIZE_LIMIT: usize = 49152;
pub struct SizeReport {
report_kind: ReportKind,
pub contracts: BTreeMap<String, ContractInfo>,
}
impl SizeReport {
pub fn max_runtime_size(&self) -> usize {
self.contracts
.values()
.filter(|c| !c.is_dev_contract)
.map(|c| c.runtime_size)
.max()
.unwrap_or(0)
}
pub fn max_init_size(&self) -> usize {
self.contracts
.values()
.filter(|c| !c.is_dev_contract)
.map(|c| c.init_size)
.max()
.unwrap_or(0)
}
pub fn exceeds_runtime_size_limit(&self) -> bool {
self.max_runtime_size() > CONTRACT_RUNTIME_SIZE_LIMIT
}
pub fn exceeds_initcode_size_limit(&self) -> bool {
self.max_init_size() > CONTRACT_INITCODE_SIZE_LIMIT
}
}
impl Display for SizeReport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
match self.report_kind {
ReportKind::Markdown => {
let table = self.format_table_output();
writeln!(f, "{table}")?;
}
ReportKind::JSON => {
writeln!(f, "{}", self.format_json_output())?;
}
}
Ok(())
}
}
impl SizeReport {
fn format_json_output(&self) -> String {
let contracts = self
.contracts
.iter()
.filter(|(_, c)| !c.is_dev_contract && (c.runtime_size > 0 || c.init_size > 0))
.map(|(name, contract)| {
(
name.clone(),
serde_json::json!({
"runtime_size": contract.runtime_size,
"init_size": contract.init_size,
"runtime_margin": CONTRACT_RUNTIME_SIZE_LIMIT as isize - contract.runtime_size as isize,
"init_margin": CONTRACT_INITCODE_SIZE_LIMIT as isize - contract.init_size as isize,
}),
)
})
.collect::<serde_json::Map<_, _>>();
serde_json::to_string(&contracts).unwrap()
}
fn format_table_output(&self) -> Table {
let mut table = Table::new();
table.load_preset(ASCII_MARKDOWN);
table.set_header([
Cell::new("Contract").add_attribute(Attribute::Bold).fg(Color::Blue),
Cell::new("Runtime Size (B)").add_attribute(Attribute::Bold).fg(Color::Blue),
Cell::new("Initcode Size (B)").add_attribute(Attribute::Bold).fg(Color::Blue),
Cell::new("Runtime Margin (B)").add_attribute(Attribute::Bold).fg(Color::Blue),
Cell::new("Initcode Margin (B)").add_attribute(Attribute::Bold).fg(Color::Blue),
]);
let contracts = self
.contracts
.iter()
.filter(|(_, c)| !c.is_dev_contract && (c.runtime_size > 0 || c.init_size > 0));
for (name, contract) in contracts {
let runtime_margin =
CONTRACT_RUNTIME_SIZE_LIMIT as isize - contract.runtime_size as isize;
let init_margin = CONTRACT_INITCODE_SIZE_LIMIT as isize - contract.init_size as isize;
let runtime_color = match contract.runtime_size {
..18_000 => Color::Reset,
18_000..=CONTRACT_RUNTIME_SIZE_LIMIT => Color::Yellow,
_ => Color::Red,
};
let init_color = match contract.init_size {
..36_000 => Color::Reset,
36_000..=CONTRACT_INITCODE_SIZE_LIMIT => Color::Yellow,
_ => Color::Red,
};
let locale = &Locale::en;
table.add_row([
Cell::new(name).fg(Color::Blue),
Cell::new(contract.runtime_size.to_formatted_string(locale))
.set_alignment(CellAlignment::Right)
.fg(runtime_color),
Cell::new(contract.init_size.to_formatted_string(locale))
.set_alignment(CellAlignment::Right)
.fg(init_color),
Cell::new(runtime_margin.to_formatted_string(locale))
.set_alignment(CellAlignment::Right)
.fg(runtime_color),
Cell::new(init_margin.to_formatted_string(locale))
.set_alignment(CellAlignment::Right)
.fg(init_color),
]);
}
table
}
}
fn contract_size<T: Artifact>(artifact: &T, initcode: bool) -> Option<usize> {
let bytecode = if initcode {
artifact.get_bytecode_object()?
} else {
artifact.get_deployed_bytecode_object()?
};
let size = match bytecode.as_ref() {
BytecodeObject::Bytecode(bytes) => bytes.len(),
BytecodeObject::Unlinked(unlinked) => {
let mut size = unlinked.len();
if unlinked.starts_with("0x") {
size -= 2;
}
size / 2
}
};
Some(size)
}
#[derive(Clone, Copy, Debug)]
pub struct ContractInfo {
pub runtime_size: usize,
pub init_size: usize,
pub is_dev_contract: bool,
}
pub fn compile_target<C: Compiler>(
target_path: &Path,
project: &Project<C>,
quiet: bool,
) -> Result<ProjectCompileOutput<C>> {
ProjectCompiler::new().quiet(quiet).files([target_path.into()]).compile(project)
}
pub fn etherscan_project(
metadata: &Metadata,
target_path: impl AsRef<Path>,
) -> Result<Project<SolcCompiler>> {
let target_path = dunce::canonicalize(target_path.as_ref())?;
let sources_path = target_path.join(&metadata.contract_name);
metadata.source_tree().write_to(&target_path)?;
let mut settings = metadata.settings()?;
for remapping in settings.remappings.iter_mut() {
let new_path = sources_path.join(remapping.path.trim_start_matches('/'));
remapping.path = new_path.display().to_string();
}
if !settings.remappings.iter().any(|remapping| remapping.name.starts_with("@openzeppelin/")) {
let oz = Remapping {
context: None,
name: "@openzeppelin/".into(),
path: sources_path.join("@openzeppelin").display().to_string(),
};
settings.remappings.push(oz);
}
let paths = ProjectPathsConfig::builder()
.sources(sources_path.clone())
.remappings(settings.remappings.clone())
.build_with_root(sources_path);
let v = metadata.compiler_version()?;
let solc = Solc::find_or_install(&v)?;
let compiler = SolcCompiler::Specific(solc);
Ok(ProjectBuilder::<SolcCompiler>::default()
.settings(SolcSettings {
settings: SolcConfig::builder().settings(settings).build(),
..Default::default()
})
.paths(paths)
.ephemeral()
.no_artifacts()
.build(compiler)?)
}
pub fn with_compilation_reporter<O>(quiet: bool, f: impl FnOnce() -> O) -> O {
#[allow(clippy::collapsible_else_if)]
let reporter = if quiet || shell::is_json() {
Report::new(NoReporter::default())
} else {
if std::io::stdout().is_terminal() {
Report::new(SpinnerReporter::spawn())
} else {
Report::new(BasicStdoutReporter::default())
}
};
foundry_compilers::report::with_scoped(&reporter, f)
}