feat(solc): allow providing --allow-args (#553)

* feat(solc): allow providing additional arguments

* feat(solc): allow providing --allow-args

* feat: more allow paths integration (#565)

* feat: more allow paths integration

* Update ethers-solc/src/compile.rs

Co-authored-by: Georgios Konstantopoulos <me@gakonst.com>

* chore: only enable macros if feature is enabled

* test: add tests for multiple libs

* chore: fix solc test

* chore: only pas --allow-paths if there are any paths needed

Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
This commit is contained in:
Georgios Konstantopoulos 2021-11-08 20:11:45 +00:00 committed by GitHub
parent dbea856e8c
commit bfdf70cbea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 170 additions and 15 deletions

View File

@ -63,7 +63,12 @@ pub static RELEASES: Lazy<Vec<Version>> = Lazy::new(|| {
///
/// Supports sync and async functions.
#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord)]
pub struct Solc(pub PathBuf);
pub struct Solc {
/// Path to the `solc` executable
pub solc: PathBuf,
/// Additional arguments passed to the `solc` exectuable
pub args: Vec<String>,
}
impl Default for Solc {
fn default() -> Self {
@ -74,7 +79,25 @@ impl Default for Solc {
impl Solc {
/// A new instance which points to `solc`
pub fn new(path: impl Into<PathBuf>) -> Self {
Solc(path.into())
Solc { solc: path.into(), args: Vec::new() }
}
/// Adds an argument to pass to the `solc` command.
pub fn arg<T: Into<String>>(mut self, arg: T) -> Self {
self.args.push(arg.into());
self
}
/// Adds multiple arguments to pass to the `solc`.
pub fn args<I, S>(mut self, args: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
for arg in args {
self = self.arg(arg);
}
self
}
/// Returns the directory in which [svm](https://github.com/roynalnaruto/svm-rs) stores all versions
@ -112,7 +135,7 @@ impl Solc {
Ok(solc)
}
/// Assuming the `versions` array is sorted, it returns the latest element which satisfies
/// Assuming the `versions` array is sorted, it returns the first element which satisfies
/// the provided [`VersionReq`]
pub fn find_matching_installation(
versions: &[Version],
@ -233,7 +256,10 @@ impl Solc {
}
pub fn compile_output<T: Serialize>(&self, input: &T) -> Result<Vec<u8>> {
let mut child = Command::new(&self.0)
let mut cmd = Command::new(&self.solc);
let mut child = cmd
.args(&self.args)
.arg("--standard-json")
.stdin(Stdio::piped())
.stderr(Stdio::piped())
@ -248,7 +274,7 @@ impl Solc {
/// Returns the version from the configured `solc`
pub fn version(&self) -> Result<Version> {
version_from_output(
Command::new(&self.0)
Command::new(&self.solc)
.arg("--version")
.stdin(Stdio::piped())
.stderr(Stdio::piped())
@ -288,7 +314,7 @@ impl Solc {
pub async fn async_compile_output<T: Serialize>(&self, input: &T) -> Result<Vec<u8>> {
use tokio::io::AsyncWriteExt;
let content = serde_json::to_vec(input)?;
let mut child = tokio::process::Command::new(&self.0)
let mut child = tokio::process::Command::new(&self.solc)
.arg("--standard-json")
.stdin(Stdio::piped())
.stderr(Stdio::piped())
@ -302,7 +328,7 @@ impl Solc {
pub async fn async_version(&self) -> Result<Version> {
version_from_output(
tokio::process::Command::new(&self.0)
tokio::process::Command::new(&self.solc)
.arg("--version")
.stdin(Stdio::piped())
.stderr(Stdio::piped())
@ -338,13 +364,13 @@ fn version_from_output(output: Output) -> Result<Version> {
impl AsRef<Path> for Solc {
fn as_ref(&self) -> &Path {
&self.0
&self.solc
}
}
impl<T: Into<PathBuf>> From<T> for Solc {
fn from(solc: T) -> Self {
Solc(solc.into())
Solc::new(solc.into())
}
}
@ -451,7 +477,7 @@ mod tests {
}
let res = Solc::find_svm_installed_version(&version.to_string()).unwrap().unwrap();
let expected = svm::SVM_HOME.join(ver).join(format!("solc-{}", ver));
assert_eq!(res.0, expected);
assert_eq!(res.solc, expected);
}
#[test]

View File

@ -272,3 +272,46 @@ impl fmt::Debug for ArtifactOutput {
}
}
}
use std::convert::TryFrom;
/// Helper struct for serializing `--allow-paths` arguments to Solc
///
/// From the [Solc docs](https://docs.soliditylang.org/en/v0.8.9/using-the-compiler.html#base-path-and-import-remapping):
/// For security reasons the compiler has restrictions on what directories it can access.
/// Directories of source files specified on the command line and target paths of
/// remappings are automatically allowed to be accessed by the file reader,
/// but everything else is rejected by default. Additional paths (and their subdirectories)
/// can be allowed via the --allow-paths /sample/path,/another/sample/path switch.
/// Everything inside the path specified via --base-path is always allowed.
#[derive(Clone, Debug, Default)]
pub struct AllowedLibPaths(pub(crate) Vec<PathBuf>);
impl fmt::Display for AllowedLibPaths {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let lib_paths = self
.0
.iter()
.filter(|path| path.exists())
.map(|path| format!("{}", path.display()))
.collect::<Vec<_>>()
.join(",");
write!(f, "{}", lib_paths)
}
}
impl<T: Into<PathBuf>> TryFrom<Vec<T>> for AllowedLibPaths {
type Error = std::io::Error;
fn try_from(libs: Vec<T>) -> std::result::Result<Self, Self::Error> {
let libs = libs
.into_iter()
.map(|lib| {
let path: PathBuf = lib.into();
let lib = std::fs::canonicalize(path)?;
Ok(lib)
})
.collect::<std::result::Result<Vec<_>, std::io::Error>>()?;
Ok(AllowedLibPaths(libs))
}
}

View File

@ -11,7 +11,7 @@ mod compile;
pub use compile::*;
mod config;
pub use config::{ArtifactOutput, ProjectPathsConfig, SolcConfig};
pub use config::{AllowedLibPaths, ArtifactOutput, ProjectPathsConfig, SolcConfig};
use crate::{artifacts::Source, cache::SolFilesCache};
@ -21,6 +21,7 @@ use crate::artifacts::Sources;
use error::Result;
use std::{
collections::{BTreeMap, HashMap},
convert::TryInto,
fmt, fs, io,
path::PathBuf,
};
@ -40,6 +41,8 @@ pub struct Project {
pub artifacts: ArtifactOutput,
/// Errors/Warnings which match these error codes are not going to be logged
pub ignored_error_codes: Vec<u64>,
/// The paths which will be allowed for library inclusion
pub allowed_lib_paths: AllowedLibPaths,
}
impl Project {
@ -116,8 +119,12 @@ impl Project {
let version = Solc::detect_version(&source)?;
// gets the solc binary for that version, it is expected tha this will succeed
// AND find the solc since it was installed right above
let solc = Solc::find_svm_installed_version(version.to_string())?
let mut solc = Solc::find_svm_installed_version(version.to_string())?
.expect("solc should have been installed");
if !self.allowed_lib_paths.0.is_empty() {
solc = solc.arg("--allow-paths").arg(self.allowed_lib_paths.to_string());
}
let entry = sources_by_version.entry(solc).or_insert_with(BTreeMap::new);
entry.insert(path, source);
}
@ -132,7 +139,6 @@ impl Project {
res.contracts.extend(compiled.contracts);
}
}
Ok(if res.contracts.is_empty() {
ProjectCompileOutput::Unchanged
} else {
@ -214,6 +220,8 @@ pub struct ProjectBuilder {
artifacts: Option<ArtifactOutput>,
/// Which error codes to ignore
pub ignored_error_codes: Vec<u64>,
/// All allowed paths
pub allowed_paths: Vec<PathBuf>,
}
impl ProjectBuilder {
@ -248,8 +256,34 @@ impl ProjectBuilder {
self
}
/// Adds an allowed-path to the solc executable
pub fn allowed_path<T: Into<PathBuf>>(mut self, path: T) -> Self {
self.allowed_paths.push(path.into());
self
}
/// Adds multiple allowed-path to the solc executable
pub fn allowed_paths<I, S>(mut self, args: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<PathBuf>,
{
for arg in args {
self = self.allowed_path(arg);
}
self
}
pub fn build(self) -> Result<Project> {
let Self { paths, solc, solc_config, cached, artifacts, ignored_error_codes } = self;
let Self {
paths,
solc,
solc_config,
cached,
artifacts,
ignored_error_codes,
mut allowed_paths,
} = self;
let solc = solc.unwrap_or_default();
let solc_config = solc_config.map(Ok).unwrap_or_else(|| {
@ -257,13 +291,21 @@ impl ProjectBuilder {
SolcConfig::builder().version(version.to_string()).build()
})?;
let paths = paths.map(Ok).unwrap_or_else(ProjectPathsConfig::current_hardhat)?;
if allowed_paths.is_empty() {
// allow every contract under root by default
allowed_paths.push(paths.root.clone())
}
Ok(Project {
paths: paths.map(Ok).unwrap_or_else(ProjectPathsConfig::current_hardhat)?,
paths,
solc,
solc_config,
cached,
artifacts: artifacts.unwrap_or_default(),
ignored_error_codes,
allowed_lib_paths: allowed_paths.try_into()?,
})
}
}
@ -277,6 +319,7 @@ impl Default for ProjectBuilder {
cached: true,
artifacts: None,
ignored_error_codes: Vec::new(),
allowed_paths: vec![],
}
}
}
@ -329,4 +372,35 @@ mod tests {
// Contracts A to F
assert_eq!(contracts.keys().count(), 5);
}
#[test]
#[cfg(all(feature = "svm", feature = "async"))]
fn test_build_many_libs() {
use super::*;
let root = std::fs::canonicalize("./test-data/test-contract-libs").unwrap();
let paths = ProjectPathsConfig::builder()
.root(&root)
.sources(root.join("src"))
.lib(root.join("lib1"))
.lib(root.join("lib2"))
.build()
.unwrap();
let project = Project::builder()
.paths(paths)
.ephemeral()
.artifacts(ArtifactOutput::Nothing)
.build()
.unwrap();
let compiled = project.compile().unwrap();
let contracts = match compiled {
ProjectCompileOutput::Compiled((out, _)) => {
assert!(!out.has_error());
out.contracts
}
_ => panic!("must compile"),
};
assert_eq!(contracts.keys().count(), 3);
}
}

View File

@ -0,0 +1,3 @@
pragma solidity ^0.8.6;
contract Bar {}

View File

@ -0,0 +1,3 @@
pragma solidity ^0.8.6;
contract Baz {}

View File

@ -0,0 +1,6 @@
pragma solidity 0.8.6;
import "../lib1/Bar.sol";
import "../lib2/Baz.sol";
contract Foo is Bar, Baz {}