//! Implements the [SolidityCompiler] trait with solc for //! compiling contracts to EVM bytecode. use std::{ path::PathBuf, process::{Command, Stdio}, sync::LazyLock, }; use dashmap::DashMap; use revive_dt_common::types::VersionOrRequirement; use revive_dt_config::Arguments; use revive_dt_solc_binaries::download_solc; use super::constants::SOLC_VERSION_SUPPORTING_VIA_YUL_IR; use crate::{CompilerInput, CompilerOutput, ModeOptimizerSetting, ModePipeline, SolidityCompiler}; use anyhow::Context; use foundry_compilers_artifacts::{ output_selection::{ BytecodeOutputSelection, ContractOutputSelection, EvmOutputSelection, OutputSelection, }, solc::CompilerOutput as SolcOutput, solc::*, }; use semver::Version; use tokio::{io::AsyncWriteExt, process::Command as AsyncCommand}; #[derive(Debug)] pub struct Solc { solc_path: PathBuf, } impl SolidityCompiler for Solc { type Options = (); #[tracing::instrument(level = "debug", ret)] async fn build( &self, CompilerInput { pipeline, optimization, evm_version, allow_paths, base_path, sources, libraries, revert_string_handling, }: CompilerInput, _: Self::Options, ) -> anyhow::Result { let compiler_supports_via_ir = self .version() .await .context("Failed to query solc version to determine via-ir support")? >= SOLC_VERSION_SUPPORTING_VIA_YUL_IR; // Be careful to entirely omit the viaIR field if the compiler does not support it, // as it will error if you provide fields it does not know about. Because // `supports_mode` is called prior to instantiating a compiler, we should never // ask for something which is invalid. let via_ir = match (pipeline, compiler_supports_via_ir) { (pipeline, true) => pipeline.map(|p| p.via_yul_ir()), (_pipeline, false) => None, }; let input = SolcInput { language: SolcLanguage::Solidity, sources: Sources( sources .into_iter() .map(|(source_path, source_code)| (source_path, Source::new(source_code))) .collect(), ), settings: Settings { optimizer: Optimizer { enabled: optimization.map(|o| o.optimizations_enabled()), details: Some(Default::default()), ..Default::default() }, output_selection: OutputSelection::common_output_selection( [ ContractOutputSelection::Abi, ContractOutputSelection::Evm(EvmOutputSelection::ByteCode( BytecodeOutputSelection::Object, )), ] .into_iter() .map(|item| item.to_string()), ), evm_version: evm_version.map(|version| version.to_string().parse().unwrap()), via_ir, libraries: Libraries { libs: libraries .into_iter() .map(|(file_path, libraries)| { ( file_path, libraries .into_iter() .map(|(library_name, library_address)| { (library_name, library_address.to_string()) }) .collect(), ) }) .collect(), }, debug: revert_string_handling.map(|revert_string_handling| DebuggingSettings { revert_strings: match revert_string_handling { crate::RevertString::Default => Some(RevertStrings::Default), crate::RevertString::Debug => Some(RevertStrings::Debug), crate::RevertString::Strip => Some(RevertStrings::Strip), crate::RevertString::VerboseDebug => Some(RevertStrings::VerboseDebug), }, debug_info: Default::default(), }), ..Default::default() }, }; let mut command = AsyncCommand::new(&self.solc_path); command .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .arg("--standard-json"); if let Some(ref base_path) = base_path { command.arg("--base-path").arg(base_path); } if !allow_paths.is_empty() { command.arg("--allow-paths").arg( allow_paths .iter() .map(|path| path.display().to_string()) .collect::>() .join(","), ); } let mut child = command .spawn() .with_context(|| format!("Failed to spawn solc at {}", self.solc_path.display()))?; let stdin = child.stdin.as_mut().expect("should be piped"); let serialized_input = serde_json::to_vec(&input) .context("Failed to serialize Standard JSON input for solc")?; stdin .write_all(&serialized_input) .await .context("Failed to write Standard JSON to solc stdin")?; let output = child .wait_with_output() .await .context("Failed while waiting for solc process to finish")?; if !output.status.success() { let json_in = serde_json::to_string_pretty(&input) .context("Failed to pretty-print Standard JSON input for logging")?; let message = String::from_utf8_lossy(&output.stderr); tracing::error!( status = %output.status, message = %message, json_input = json_in, "Compilation using solc failed" ); anyhow::bail!("Compilation failed with an error: {message}"); } let parsed = serde_json::from_slice::(&output.stdout) .map_err(|e| { anyhow::anyhow!( "failed to parse resolc JSON output: {e}\nstderr: {}", String::from_utf8_lossy(&output.stdout) ) }) .context("Failed to parse solc standard JSON output")?; // Detecting if the compiler output contained errors and reporting them through logs and // errors instead of returning the compiler output that might contain errors. for error in parsed.errors.iter() { if error.severity == Severity::Error { tracing::error!(?error, ?input, "Encountered an error in the compilation"); anyhow::bail!("Encountered an error in the compilation: {error}") } } tracing::debug!( output = %String::from_utf8_lossy(&output.stdout).to_string(), "Compiled successfully" ); let mut compiler_output = CompilerOutput::default(); for (contract_path, contracts) in parsed.contracts { let map = compiler_output .contracts .entry(contract_path.canonicalize().with_context(|| { format!( "Failed to canonicalize contract path {}", contract_path.display() ) })?) .or_default(); for (contract_name, contract_info) in contracts.into_iter() { let source_code = contract_info .evm .and_then(|evm| evm.bytecode) .map(|bytecode| match bytecode.object { BytecodeObject::Bytecode(bytecode) => bytecode.to_string(), BytecodeObject::Unlinked(unlinked) => unlinked, }) .context("Unexpected - contract compiled with solc has no source code")?; let abi = contract_info .abi .context("Unexpected - contract compiled with solc as no ABI")?; map.insert(contract_name, (source_code, abi)); } } Ok(compiler_output) } fn new(solc_path: PathBuf) -> Self { Self { solc_path } } async fn get_compiler_executable( config: &Arguments, version: impl Into, ) -> anyhow::Result { let path = download_solc(config.directory(), version, config.wasm) .await .context("Failed to download/get path to solc binary")?; Ok(path) } async fn version(&self) -> anyhow::Result { /// This is a cache of the path of the compiler to the version number of the compiler. We /// choose to cache the version in this way rather than through a field on the struct since /// compiler objects are being created all the time from the path and the compiler object is /// not reused over time. static VERSION_CACHE: LazyLock> = LazyLock::new(Default::default); match VERSION_CACHE.entry(self.solc_path.clone()) { dashmap::Entry::Occupied(occupied_entry) => Ok(occupied_entry.get().clone()), dashmap::Entry::Vacant(vacant_entry) => { // The following is the parsing code for the version from the solc version strings // which look like the following: // ``` // solc, the solidity compiler commandline interface // Version: 0.8.30+commit.73712a01.Darwin.appleclang // ``` let child = Command::new(self.solc_path.as_path()) .arg("--version") .stdout(Stdio::piped()) .spawn() .with_context(|| { format!( "Failed to spawn solc at {} to get version", self.solc_path.display() ) })?; let output = child.wait_with_output().with_context(|| { format!( "Failed waiting for solc at {} to finish --version", self.solc_path.display() ) })?; let output = String::from_utf8_lossy(&output.stdout); let version_line = output .split("Version: ") .nth(1) .context("Version parsing failed")?; let version_string = version_line .split("+") .next() .context("Version parsing failed")?; let version = Version::parse(version_string).with_context(|| { format!("Failed to parse solc semver from '{version_string}'") })?; vacant_entry.insert(version.clone()); Ok(version) } } } fn supports_mode( compiler_version: &Version, _optimize_setting: ModeOptimizerSetting, pipeline: ModePipeline, ) -> bool { // solc 0.8.13 and above supports --via-ir, and less than that does not. Thus, we support mode E // (ie no Yul IR) in either case, but only support Y (via Yul IR) if the compiler is new enough. pipeline == ModePipeline::ViaEVMAssembly || (pipeline == ModePipeline::ViaYulIR && compiler_version >= &SOLC_VERSION_SUPPORTING_VIA_YUL_IR) } } #[cfg(test)] mod test { use super::*; #[tokio::test] async fn compiler_version_can_be_obtained() { // Arrange let args = Arguments::default(); let path = Solc::get_compiler_executable(&args, Version::new(0, 7, 6)) .await .unwrap(); let compiler = Solc::new(path); // Act let version = compiler.version().await; // Assert assert_eq!( version.expect("Failed to get version"), Version::new(0, 7, 6) ) } #[tokio::test] async fn compiler_version_can_be_obtained1() { // Arrange let args = Arguments::default(); let path = Solc::get_compiler_executable(&args, Version::new(0, 4, 21)) .await .unwrap(); let compiler = Solc::new(path); // Act let version = compiler.version().await; // Assert assert_eq!( version.expect("Failed to get version"), Version::new(0, 4, 21) ) } }