fix(solc): remapping aware libraries (#1190)
* feat(solc): add Libraries type * feat: add lib applied remappings * test: add lib linking tests * Update ethers-solc/src/artifacts/mod.rs Co-authored-by: Georgios Konstantopoulos <me@gakonst.com> * Update ethers-solc/src/artifacts/mod.rs Co-authored-by: Georgios Konstantopoulos <me@gakonst.com> * Update ethers-solc/src/artifacts/mod.rs Co-authored-by: Georgios Konstantopoulos <me@gakonst.com> * Update ethers-solc/src/artifacts/mod.rs Co-authored-by: Georgios Konstantopoulos <me@gakonst.com> * chore: rustfmt Co-authored-by: Georgios Konstantopoulos <me@gakonst.com>
This commit is contained in:
parent
19f7a93243
commit
a978bc98af
|
@ -11,9 +11,12 @@ use std::{
|
|||
str::FromStr,
|
||||
};
|
||||
|
||||
use crate::{compile::*, error::SolcIoError, remappings::Remapping, utils};
|
||||
use crate::{
|
||||
compile::*, error::SolcIoError, remappings::Remapping, utils, ProjectPathsConfig, SolcError,
|
||||
};
|
||||
|
||||
use serde::{de::Visitor, Deserialize, Deserializer, Serialize, Serializer};
|
||||
use tracing::warn;
|
||||
|
||||
pub mod ast;
|
||||
pub use ast::*;
|
||||
|
@ -259,8 +262,15 @@ pub struct Settings {
|
|||
pub via_ir: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub debug: Option<DebuggingSettings>,
|
||||
#[serde(default, skip_serializing_if = "::std::collections::BTreeMap::is_empty")]
|
||||
pub libraries: BTreeMap<String, BTreeMap<String, String>>,
|
||||
/// Addresses of the libraries. If not all libraries are given here,
|
||||
/// it can result in unlinked objects whose output data is different.
|
||||
///
|
||||
/// The top level key is the name of the source file where the library is used.
|
||||
/// If remappings are used, this source file should match the global path
|
||||
/// after remappings were applied.
|
||||
/// If this key is an empty string, that refers to a global level.
|
||||
#[serde(default, skip_serializing_if = "Libraries::is_empty")]
|
||||
pub libraries: Libraries,
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
|
@ -381,6 +391,101 @@ impl Default for Settings {
|
|||
}
|
||||
}
|
||||
|
||||
/// A wrapper type for all libraries in the form of `<file>:<lib>:<addr>`
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
#[serde(transparent)]
|
||||
pub struct Libraries {
|
||||
/// All libraries, `(file path -> (Lib name -> Address))
|
||||
pub libs: BTreeMap<PathBuf, BTreeMap<String, String>>,
|
||||
}
|
||||
|
||||
// === impl Libraries ===
|
||||
|
||||
impl Libraries {
|
||||
/// Parses all libraries in the form of
|
||||
/// `<file>:<lib>:<addr>`
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use ethers_solc::artifacts::Libraries;
|
||||
/// let libs = Libraries::parse(&[
|
||||
/// "src/DssSpell.sol:DssExecLib:0xfD88CeE74f7D78697775aBDAE53f9Da1559728E4".to_string(),
|
||||
/// ])
|
||||
/// .unwrap();
|
||||
/// ```
|
||||
pub fn parse(libs: &[String]) -> Result<Self, SolcError> {
|
||||
let mut libraries = BTreeMap::default();
|
||||
for lib in libs {
|
||||
let mut items = lib.split(':');
|
||||
let file = items.next().ok_or_else(|| {
|
||||
SolcError::msg(format!("failed to parse path to library file: {}", lib))
|
||||
})?;
|
||||
let lib = items
|
||||
.next()
|
||||
.ok_or_else(|| SolcError::msg(format!("failed to parse library name: {}", lib)))?;
|
||||
let addr = items.next().ok_or_else(|| {
|
||||
SolcError::msg(format!("failed to parse library address: {}", lib))
|
||||
})?;
|
||||
if items.next().is_some() {
|
||||
return Err(SolcError::msg(format!(
|
||||
"failed to parse, too many arguments passed: {}",
|
||||
lib
|
||||
)))
|
||||
}
|
||||
libraries
|
||||
.entry(file.into())
|
||||
.or_insert_with(BTreeMap::default)
|
||||
.insert(lib.to_string(), addr.to_string());
|
||||
}
|
||||
Ok(Self { libs: libraries })
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.libs.is_empty()
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.libs.len()
|
||||
}
|
||||
|
||||
/// Solc expects the lib paths to match the global path after remappings were applied
|
||||
///
|
||||
/// See also [ProjectPathsConfig::resolve_import]
|
||||
pub fn with_applied_remappings(mut self, config: &ProjectPathsConfig) -> Self {
|
||||
self.libs = self
|
||||
.libs
|
||||
.into_iter()
|
||||
.map(|(file, target)| {
|
||||
let file = config.resolve_import(&config.root, &file).unwrap_or_else(|err| {
|
||||
warn!(target: "libs", "Failed to resolve library `{}` for linking: {:?}", file.display(), err);
|
||||
file
|
||||
});
|
||||
(file, target)
|
||||
})
|
||||
.collect();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BTreeMap<PathBuf, BTreeMap<String, String>>> for Libraries {
|
||||
fn from(libs: BTreeMap<PathBuf, BTreeMap<String, String>>) -> Self {
|
||||
Self { libs }
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<BTreeMap<PathBuf, BTreeMap<String, String>>> for Libraries {
|
||||
fn as_ref(&self) -> &BTreeMap<PathBuf, BTreeMap<String, String>> {
|
||||
&self.libs
|
||||
}
|
||||
}
|
||||
|
||||
impl AsMut<BTreeMap<PathBuf, BTreeMap<String, String>>> for Libraries {
|
||||
fn as_mut(&mut self) -> &mut BTreeMap<PathBuf, BTreeMap<String, String>> {
|
||||
&mut self.libs
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Optimizer {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
|
@ -772,13 +877,13 @@ pub struct Doc {
|
|||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub kind: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub methods: Option<Libraries>,
|
||||
pub methods: Option<DocLibraries>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub version: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
pub struct Libraries {
|
||||
pub struct DocLibraries {
|
||||
#[serde(flatten)]
|
||||
pub libs: BTreeMap<String, serde_json::Value>,
|
||||
}
|
||||
|
@ -1631,4 +1736,71 @@ mod tests {
|
|||
let i = input.sanitized(&version);
|
||||
assert!(i.settings.metadata.unwrap().bytecode_hash.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_parse_libraries() {
|
||||
let libraries = ["./src/lib/LibraryContract.sol:Library:0xaddress".to_string()];
|
||||
|
||||
let libs = Libraries::parse(&libraries[..]).unwrap().libs;
|
||||
|
||||
assert_eq!(
|
||||
libs,
|
||||
BTreeMap::from([(
|
||||
PathBuf::from("./src/lib/LibraryContract.sol"),
|
||||
BTreeMap::from([("Library".to_string(), "0xaddress".to_string())])
|
||||
)])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_parse_many_libraries() {
|
||||
let libraries= [
|
||||
"./src/SizeAuctionDiscount.sol:Chainlink:0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5".to_string(),
|
||||
"./src/SizeAuction.sol:ChainlinkTWAP:0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5".to_string(),
|
||||
"./src/SizeAuction.sol:Math:0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c".to_string(),
|
||||
"./src/test/ChainlinkTWAP.t.sol:ChainlinkTWAP:0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5".to_string(),
|
||||
"./src/SizeAuctionDiscount.sol:Math:0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c".to_string(),
|
||||
];
|
||||
|
||||
let libs = Libraries::parse(&libraries[..]).unwrap().libs;
|
||||
|
||||
pretty_assertions::assert_eq!(
|
||||
libs,
|
||||
BTreeMap::from([
|
||||
(
|
||||
PathBuf::from("./src/SizeAuctionDiscount.sol"),
|
||||
BTreeMap::from([
|
||||
(
|
||||
"Chainlink".to_string(),
|
||||
"0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5".to_string()
|
||||
),
|
||||
(
|
||||
"Math".to_string(),
|
||||
"0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c".to_string()
|
||||
)
|
||||
])
|
||||
),
|
||||
(
|
||||
PathBuf::from("./src/SizeAuction.sol"),
|
||||
BTreeMap::from([
|
||||
(
|
||||
"ChainlinkTWAP".to_string(),
|
||||
"0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5".to_string()
|
||||
),
|
||||
(
|
||||
"Math".to_string(),
|
||||
"0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c".to_string()
|
||||
)
|
||||
])
|
||||
),
|
||||
(
|
||||
PathBuf::from("./src/test/ChainlinkTWAP.t.sol"),
|
||||
BTreeMap::from([(
|
||||
"ChainlinkTWAP".to_string(),
|
||||
"0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5".to_string()
|
||||
)])
|
||||
),
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -650,7 +650,7 @@ impl<T: Into<PathBuf>> From<T> for Solc {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::CompilerInput;
|
||||
use crate::{Artifact, CompilerInput};
|
||||
|
||||
fn solc() -> Solc {
|
||||
Solc::default()
|
||||
|
@ -692,6 +692,18 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_compile_with_remapped_links() {
|
||||
let input: CompilerInput =
|
||||
serde_json::from_str(include_str!("../../test-data/library-remapping-in.json"))
|
||||
.unwrap();
|
||||
let out = solc().compile(&input).unwrap();
|
||||
let (_, mut contracts) = out.split();
|
||||
let contract = contracts.remove("LinkTest").unwrap();
|
||||
let bytecode = &contract.get_bytecode().unwrap().object;
|
||||
assert!(!bytecode.is_unlinked());
|
||||
}
|
||||
|
||||
#[cfg(feature = "async")]
|
||||
#[tokio::test]
|
||||
async fn async_solc_compile_works() {
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"language": "Solidity",
|
||||
"sources": {
|
||||
"/private/var/folders/l5/lprhf87s6xv8djgd017f0b2h0000gn/T/tmp_dappPyXsdD/lib/remapping/MyLib.sol": {
|
||||
"content": "\n// SPDX-License-Identifier: MIT\nlibrary MyLib {\n function foobar(uint256 a) public view returns (uint256) {\n \treturn a * 100;\n }\n}\n"
|
||||
},
|
||||
"/private/var/folders/l5/lprhf87s6xv8djgd017f0b2h0000gn/T/tmp_dappPyXsdD/src/LinkTest.sol": {
|
||||
"content": "\n// SPDX-License-Identifier: MIT\nimport \"remapping/MyLib.sol\";\ncontract LinkTest {\n function foo() public returns (uint256) {\n return MyLib.foobar(1);\n }\n}\n"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"remappings": [
|
||||
"remapping/=/private/var/folders/l5/lprhf87s6xv8djgd017f0b2h0000gn/T/tmp_dappPyXsdD/lib/remapping/"
|
||||
],
|
||||
"optimizer": {
|
||||
"enabled": false,
|
||||
"runs": 200
|
||||
},
|
||||
"outputSelection": {
|
||||
"*": {
|
||||
"": [
|
||||
"ast"
|
||||
],
|
||||
"*": [
|
||||
"abi",
|
||||
"evm.bytecode",
|
||||
"evm.deployedBytecode",
|
||||
"evm.methodIdentifiers"
|
||||
]
|
||||
}
|
||||
},
|
||||
"evmVersion": "london",
|
||||
"libraries": {
|
||||
"/private/var/folders/l5/lprhf87s6xv8djgd017f0b2h0000gn/T/tmp_dappPyXsdD/lib/remapping/MyLib.sol": {
|
||||
"MyLib": "0x0000000000000000000000000000000000000000"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,14 +1,15 @@
|
|||
//! project tests
|
||||
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
collections::{BTreeMap, HashMap, HashSet},
|
||||
io,
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use ethers_core::types::Address;
|
||||
use ethers_solc::{
|
||||
artifacts::BytecodeHash,
|
||||
artifacts::{BytecodeHash, Libraries},
|
||||
cache::{SolFilesCache, SOLIDITY_FILES_CACHE_FILENAME},
|
||||
project_util::*,
|
||||
remappings::Remapping,
|
||||
|
@ -155,7 +156,7 @@ fn can_compile_dapp_detect_changes_in_libs() {
|
|||
project
|
||||
.paths_mut()
|
||||
.remappings
|
||||
.push(Remapping::from_str(&format!("remapping={}/", remapping.display())).unwrap());
|
||||
.push(Remapping::from_str(&format!("remapping/={}/", remapping.display())).unwrap());
|
||||
|
||||
let src = project
|
||||
.add_source(
|
||||
|
@ -814,6 +815,130 @@ contract LinkTest {
|
|||
assert_eq!(bytecode.clone(), serde_json::from_str(&s).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_apply_libraries() {
|
||||
let mut tmp = TempProject::dapptools().unwrap();
|
||||
|
||||
tmp.add_source(
|
||||
"LinkTest",
|
||||
r#"
|
||||
// SPDX-License-Identifier: MIT
|
||||
import "./MyLib.sol";
|
||||
contract LinkTest {
|
||||
function foo() public returns (uint256) {
|
||||
return MyLib.foobar(1);
|
||||
}
|
||||
}
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let lib = tmp
|
||||
.add_source(
|
||||
"MyLib",
|
||||
r#"
|
||||
// SPDX-License-Identifier: MIT
|
||||
library MyLib {
|
||||
function foobar(uint256 a) public view returns (uint256) {
|
||||
return a * 100;
|
||||
}
|
||||
}
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let compiled = tmp.compile().unwrap();
|
||||
assert!(!compiled.has_compiler_errors());
|
||||
|
||||
assert!(compiled.find("MyLib").is_some());
|
||||
let contract = compiled.find("LinkTest").unwrap();
|
||||
let bytecode = &contract.bytecode.as_ref().unwrap().object;
|
||||
assert!(bytecode.is_unlinked());
|
||||
|
||||
// provide the library settings to let solc link
|
||||
tmp.project_mut().solc_config.settings.libraries = BTreeMap::from([(
|
||||
lib,
|
||||
BTreeMap::from([("MyLib".to_string(), format!("{:?}", Address::zero()))]),
|
||||
)])
|
||||
.into();
|
||||
|
||||
let compiled = tmp.compile().unwrap();
|
||||
assert!(!compiled.has_compiler_errors());
|
||||
|
||||
assert!(compiled.find("MyLib").is_some());
|
||||
let contract = compiled.find("LinkTest").unwrap();
|
||||
let bytecode = &contract.bytecode.as_ref().unwrap().object;
|
||||
assert!(!bytecode.is_unlinked());
|
||||
|
||||
let libs = Libraries::parse(&[format!("./src/MyLib.sol:MyLib:{:?}", Address::zero())]).unwrap();
|
||||
// provide the library settings to let solc link
|
||||
tmp.project_mut().solc_config.settings.libraries = libs.with_applied_remappings(tmp.paths());
|
||||
|
||||
let compiled = tmp.compile().unwrap();
|
||||
assert!(!compiled.has_compiler_errors());
|
||||
|
||||
assert!(compiled.find("MyLib").is_some());
|
||||
let contract = compiled.find("LinkTest").unwrap();
|
||||
let bytecode = &contract.bytecode.as_ref().unwrap().object;
|
||||
assert!(!bytecode.is_unlinked());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_apply_libraries_with_remappings() {
|
||||
let mut tmp = TempProject::dapptools().unwrap();
|
||||
|
||||
let remapping = tmp.paths().libraries[0].join("remapping");
|
||||
tmp.paths_mut()
|
||||
.remappings
|
||||
.push(Remapping::from_str(&format!("remapping/={}/", remapping.display())).unwrap());
|
||||
|
||||
tmp.add_source(
|
||||
"LinkTest",
|
||||
r#"
|
||||
// SPDX-License-Identifier: MIT
|
||||
import "remapping/MyLib.sol";
|
||||
contract LinkTest {
|
||||
function foo() public returns (uint256) {
|
||||
return MyLib.foobar(1);
|
||||
}
|
||||
}
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
tmp.add_lib(
|
||||
"remapping/MyLib",
|
||||
r#"
|
||||
// SPDX-License-Identifier: MIT
|
||||
library MyLib {
|
||||
function foobar(uint256 a) public view returns (uint256) {
|
||||
return a * 100;
|
||||
}
|
||||
}
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let compiled = tmp.compile().unwrap();
|
||||
assert!(!compiled.has_compiler_errors());
|
||||
|
||||
assert!(compiled.find("MyLib").is_some());
|
||||
let contract = compiled.find("LinkTest").unwrap();
|
||||
let bytecode = &contract.bytecode.as_ref().unwrap().object;
|
||||
assert!(bytecode.is_unlinked());
|
||||
|
||||
let libs =
|
||||
Libraries::parse(&[format!("remapping/MyLib.sol:MyLib:{:?}", Address::zero())]).unwrap(); // provide the library settings to let solc link
|
||||
tmp.project_mut().solc_config.settings.libraries = libs.with_applied_remappings(tmp.paths());
|
||||
|
||||
let compiled = tmp.compile().unwrap();
|
||||
assert!(!compiled.has_compiler_errors());
|
||||
|
||||
assert!(compiled.find("MyLib").is_some());
|
||||
let contract = compiled.find("LinkTest").unwrap();
|
||||
let bytecode = &contract.bytecode.as_ref().unwrap().object;
|
||||
assert!(!bytecode.is_unlinked());
|
||||
}
|
||||
#[test]
|
||||
fn can_recompile_with_changes() {
|
||||
let mut tmp = TempProject::dapptools().unwrap();
|
||||
|
|
Loading…
Reference in New Issue