Use the solc version required in tests rather than one on PATH

This commit is contained in:
James Wilson
2025-08-21 12:23:54 +01:00
parent 85033cfead
commit 491a9a32b2
11 changed files with 263 additions and 307 deletions
+31 -27
View File
@@ -4,6 +4,7 @@
//! - Polkadot revive Wasm compiler
mod constants;
mod utils;
use std::{
collections::HashMap,
@@ -13,12 +14,11 @@ use std::{
use alloy::json_abi::JsonAbi;
use alloy_primitives::Address;
use semver::Version;
use semver::VersionReq;
use serde::{Deserialize, Serialize};
use revive_common::EVMVersion;
use revive_dt_common::cached_fs::read_to_string;
use revive_dt_common::types::VersionOrRequirement;
use revive_dt_config::Arguments;
// Re-export this as it's a part of the compiler interface.
@@ -40,28 +40,20 @@ pub trait SolidityCompiler {
additional_options: Self::Options,
) -> impl Future<Output = anyhow::Result<CompilerOutput>>;
fn new(solc_executable: PathBuf) -> Self;
fn get_compiler_executable(
config: &Arguments,
version: impl Into<VersionOrRequirement>,
) -> impl Future<Output = anyhow::Result<PathBuf>>;
fn version(&self) -> impl Future<Output = anyhow::Result<Version>>;
/// Instantiate a new compiler.
fn new(config: &Arguments) -> Self;
/// Does the compiler support the provided mode and version settings?
fn supports_mode(
compiler_version: &Version,
optimize_setting: ModeOptimizerSetting,
pipeline: ModePipeline,
) -> bool;
fn supports_mode(optimize_setting: ModeOptimizerSetting, pipeline: ModePipeline) -> bool;
}
/// The generic compilation input configuration.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize)]
pub struct CompilerInput {
pub wasm: bool,
pub pipeline: Option<ModePipeline>,
pub optimization: Option<ModeOptimizerSetting>,
pub solc_version: Option<VersionReq>,
pub evm_version: Option<EVMVersion>,
pub allow_paths: Vec<PathBuf>,
pub base_path: Option<PathBuf>,
@@ -84,12 +76,6 @@ pub struct Compiler<T: SolidityCompiler> {
additional_options: T::Options,
}
impl Default for Compiler<solc::Solc> {
fn default() -> Self {
Self::new()
}
}
impl<T> Compiler<T>
where
T: SolidityCompiler,
@@ -97,8 +83,10 @@ where
pub fn new() -> Self {
Self {
input: CompilerInput {
wasm: Default::default(),
pipeline: Default::default(),
optimization: Default::default(),
solc_version: Default::default(),
evm_version: Default::default(),
allow_paths: Default::default(),
base_path: Default::default(),
@@ -110,6 +98,16 @@ where
}
}
pub fn with_wasm(mut self, value: bool) -> Self {
self.input.wasm = value;
self
}
pub fn with_solc_version_req(mut self, value: impl Into<Option<VersionReq>>) -> Self {
self.input.solc_version = value.into();
self
}
pub fn with_optimization(mut self, value: impl Into<Option<ModeOptimizerSetting>>) -> Self {
self.input.optimization = value.into();
self
@@ -177,11 +175,8 @@ where
callback(self)
}
pub async fn try_build(
self,
compiler_path: impl AsRef<Path>,
) -> anyhow::Result<CompilerOutput> {
T::new(compiler_path.as_ref().to_path_buf())
pub async fn try_build(self, config: &Arguments) -> anyhow::Result<CompilerOutput> {
T::new(config)
.build(self.input, self.additional_options)
.await
}
@@ -191,6 +186,15 @@ where
}
}
impl<T> Default for Compiler<T>
where
T: SolidityCompiler,
{
fn default() -> Self {
Self::new()
}
}
/// Defines how the compiler should handle revert strings.
#[derive(
Clone, Debug, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize,
+32 -83
View File
@@ -1,13 +1,8 @@
//! Implements the [SolidityCompiler] trait with `resolc` for
//! compiling contracts to PolkaVM (PVM) bytecode.
use std::{
path::PathBuf,
process::{Command, Stdio},
sync::LazyLock,
};
use std::{path::PathBuf, process::Stdio};
use dashmap::DashMap;
use revive_dt_common::types::VersionOrRequirement;
use revive_dt_config::Arguments;
use revive_solc_json_interface::{
@@ -17,6 +12,7 @@ use revive_solc_json_interface::{
};
use super::constants::SOLC_VERSION_SUPPORTING_VIA_YUL_IR;
use super::utils;
use crate::{CompilerInput, CompilerOutput, ModeOptimizerSetting, ModePipeline, SolidityCompiler};
use alloy::json_abi::JsonAbi;
@@ -31,6 +27,11 @@ use tokio::{io::AsyncWriteExt, process::Command as AsyncCommand};
/// A wrapper around the `resolc` binary, emitting PVM-compatible bytecode.
#[derive(Debug)]
pub struct Resolc {
// Where to cache artifacts.
cache_directory: PathBuf,
// We'll use this version when no explicit version
// requirement is given in the test mode.
solc_version: Version,
/// Path to the `resolc` executable
resolc_path: PathBuf,
}
@@ -42,8 +43,10 @@ impl SolidityCompiler for Resolc {
async fn build(
&self,
CompilerInput {
wasm,
pipeline,
optimization,
solc_version,
evm_version,
allow_paths,
base_path,
@@ -61,6 +64,19 @@ impl SolidityCompiler for Resolc {
);
}
let solc_version_req = solc_version
.unwrap_or_else(|| VersionOrRequirement::version_to_requirement(&self.solc_version));
let solc_path =
revive_dt_solc_binaries::download_solc(&self.cache_directory, solc_version_req, wasm)
.await?;
let solc_version = utils::solc_version(&solc_path).await?;
if solc_version < SOLC_VERSION_SUPPORTING_VIA_YUL_IR {
anyhow::bail!(
"We are trying to run the test with solc version {solc_version}, but require {SOLC_VERSION_SUPPORTING_VIA_YUL_IR} or greater"
);
}
let input = SolcStandardJsonInput {
language: SolcStandardJsonInputLanguage::Solidity,
sources: sources
@@ -106,6 +122,8 @@ impl SolidityCompiler for Resolc {
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.arg("--solc")
.arg(&solc_path)
.arg("--standard-json");
if let Some(ref base_path) = base_path {
@@ -206,86 +224,17 @@ impl SolidityCompiler for Resolc {
Ok(compiler_output)
}
fn new(resolc_path: PathBuf) -> Self {
Resolc { resolc_path }
}
async fn get_compiler_executable(
config: &Arguments,
_version: impl Into<VersionOrRequirement>,
) -> anyhow::Result<PathBuf> {
if !config.resolc.as_os_str().is_empty() {
return Ok(config.resolc.clone());
}
Ok(PathBuf::from("resolc"))
}
async fn version(&self) -> anyhow::Result<semver::Version> {
/// 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<DashMap<PathBuf, Version>> = LazyLock::new(Default::default);
match VERSION_CACHE.entry(self.resolc_path.clone()) {
dashmap::Entry::Occupied(occupied_entry) => Ok(occupied_entry.get().clone()),
dashmap::Entry::Vacant(vacant_entry) => {
let output = Command::new(self.resolc_path.as_path())
.arg("--version")
.stdout(Stdio::piped())
.spawn()?
.wait_with_output()?
.stdout;
let output = String::from_utf8_lossy(&output);
let version_string = output
.split("version ")
.nth(1)
.context("Version parsing failed")?
.split("+")
.next()
.context("Version parsing failed")?;
let version = Version::parse(version_string)?;
vacant_entry.insert(version.clone());
Ok(version)
}
fn new(config: &Arguments) -> Self {
Resolc {
cache_directory: config.directory().to_path_buf(),
solc_version: config.solc.clone(),
resolc_path: config.resolc.clone(),
}
}
fn supports_mode(
compiler_version: &Version,
_optimize_setting: ModeOptimizerSetting,
pipeline: ModePipeline,
) -> bool {
// We only support the Y (IE compile via Yul IR) mode here, which also means that we can
// only use solc version 0.8.13 and above. We must always compile via Yul IR as resolc
// needs this to translate to LLVM IR and then RISCV.
fn supports_mode(_optimize_setting: ModeOptimizerSetting, pipeline: ModePipeline) -> bool {
// We only support the Y (IE compile via Yul IR) mode here. We must always compile
// via Yul IR as resolc needs this to translate to LLVM IR and then RISCV.
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 = Resolc::get_compiler_executable(&args, Version::new(0, 7, 6))
.await
.unwrap();
let compiler = Resolc::new(path);
// Act
let version = compiler.version().await;
// Assert
let _ = version.expect("Failed to get version");
}
}
+23 -110
View File
@@ -1,18 +1,13 @@
//! Implements the [SolidityCompiler] trait with solc for
//! compiling contracts to EVM bytecode.
use std::{
path::PathBuf,
process::{Command, Stdio},
sync::LazyLock,
};
use std::{path::PathBuf, process::Stdio};
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 super::utils;
use crate::{CompilerInput, CompilerOutput, ModeOptimizerSetting, ModePipeline, SolidityCompiler};
use anyhow::Context;
@@ -28,7 +23,11 @@ use tokio::{io::AsyncWriteExt, process::Command as AsyncCommand};
#[derive(Debug)]
pub struct Solc {
solc_path: PathBuf,
// Where to cache artifacts.
cache_directory: PathBuf,
// We'll use this version when no explicit version requirement
// is given in the test mode.
solc_version: Version,
}
impl SolidityCompiler for Solc {
@@ -38,8 +37,10 @@ impl SolidityCompiler for Solc {
async fn build(
&self,
CompilerInput {
wasm,
pipeline,
optimization,
solc_version,
evm_version,
allow_paths,
base_path,
@@ -49,7 +50,13 @@ impl SolidityCompiler for Solc {
}: CompilerInput,
_: Self::Options,
) -> anyhow::Result<CompilerOutput> {
let compiler_supports_via_ir = self.version().await? >= SOLC_VERSION_SUPPORTING_VIA_YUL_IR;
let solc_version = solc_version
.unwrap_or_else(|| VersionOrRequirement::version_to_requirement(&self.solc_version));
let solc_path =
revive_dt_solc_binaries::download_solc(&self.cache_directory, solc_version, wasm)
.await?;
let compiler_supports_via_ir =
utils::solc_version(&solc_path).await? >= 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
@@ -115,7 +122,7 @@ impl SolidityCompiler for Solc {
},
};
let mut command = AsyncCommand::new(&self.solc_path);
let mut command = AsyncCommand::new(&solc_path);
command
.stdin(Stdio::piped())
.stdout(Stdio::piped())
@@ -199,110 +206,16 @@ impl SolidityCompiler for Solc {
Ok(compiler_output)
}
fn new(solc_path: PathBuf) -> Self {
Self { solc_path }
}
async fn get_compiler_executable(
config: &Arguments,
version: impl Into<VersionOrRequirement>,
) -> anyhow::Result<PathBuf> {
let path = download_solc(config.directory(), version, config.wasm).await?;
Ok(path)
}
async fn version(&self) -> anyhow::Result<semver::Version> {
/// 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<DashMap<PathBuf, Version>> = 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()?;
let output = child.wait_with_output()?;
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)?;
vacant_entry.insert(version.clone());
Ok(version)
}
fn new(config: &Arguments) -> Self {
Self {
cache_directory: config.directory().to_path_buf(),
solc_version: config.solc.clone(),
}
}
fn supports_mode(
compiler_version: &Version,
_optimize_setting: ModeOptimizerSetting,
pipeline: ModePipeline,
) -> bool {
fn supports_mode(_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)
)
pipeline == ModePipeline::ViaEVMAssembly || pipeline == ModePipeline::ViaYulIR
}
}
+50
View File
@@ -0,0 +1,50 @@
use std::{
path::{Path, PathBuf},
process::{Command, Stdio},
sync::LazyLock,
};
use anyhow::Context;
use dashmap::DashMap;
use semver::Version;
/// Fetch the solc version given a path to the executable
pub async fn solc_version(solc_path: &Path) -> anyhow::Result<semver::Version> {
/// 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<DashMap<PathBuf, Version>> = LazyLock::new(Default::default);
match VERSION_CACHE.entry(solc_path.to_path_buf()) {
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(solc_path)
.arg("--version")
.stdout(Stdio::piped())
.spawn()?;
let output = child.wait_with_output()?;
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)?;
vacant_entry.insert(version.clone());
Ok(version)
}
}
}