Skip to content
This repository has been archived by the owner on Oct 19, 2024. It is now read-only.

feat(solc): resolve absolute imports in libraries #1590

Merged
merged 12 commits into from
Aug 12, 2022
9 changes: 9 additions & 0 deletions ethers-solc/src/compile/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ pub const BERLIN_SOLC: Version = Version::new(0, 8, 5);
/// <https://blog.soliditylang.org/2021/08/11/solidity-0.8.7-release-announcement/>
pub const LONDON_SOLC: Version = Version::new(0, 8, 7);

// `--base-path` was introduced in 0.6.9 <https://github.com/ethereum/solidity/releases/tag/v0.6.9>
pub static SUPPORTS_BASE_PATH: once_cell::sync::Lazy<VersionReq> =
once_cell::sync::Lazy::new(|| VersionReq::parse(">=0.6.9").unwrap());

// `--include-path` was introduced in 0.8.8 <https://github.com/ethereum/solidity/releases/tag/v0.8.8>
pub static SUPPORTS_INCLUDE_PATH: once_cell::sync::Lazy<VersionReq> =
once_cell::sync::Lazy::new(|| VersionReq::parse(">=0.8.8").unwrap());

#[cfg(any(test, feature = "tests"))]
use std::sync::Mutex;

Expand Down Expand Up @@ -527,6 +535,7 @@ impl Solc {
let mut cmd = Command::new(&self.solc);
if let Some(ref base_path) = self.base_path {
cmd.current_dir(base_path);
cmd.arg("--base-path").arg(base_path);
}
let mut child = cmd
.args(&self.args)
Expand Down
11 changes: 9 additions & 2 deletions ethers-solc/src/compile/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,7 @@ impl<'a, T: ArtifactOutput> ProjectCompiler<'a, T> {
let graph = Graph::resolve_sources(&project.paths, sources)?;
let (versions, edges) = graph.into_sources_by_version(project.offline)?;

let base_path = project.root();
let sources_by_version = versions.get(&project.allowed_lib_paths, base_path)?;
let sources_by_version = versions.get(project)?;

let sources = if project.solc_jobs > 1 && sources_by_version.len() > 1 {
// if there are multiple different versions, and we can use multiple jobs we can compile
Expand All @@ -178,6 +177,14 @@ impl<'a, T: ArtifactOutput> ProjectCompiler<'a, T> {
) -> Result<Self> {
let version = solc.version()?;
let (sources, edges) = Graph::resolve_sources(&project.paths, sources)?.into_sources();

// make sure `solc` has all required arguments
let solc = project.configure_solc_with_version(
solc,
Some(version.clone()),
edges.include_paths().clone(),
);

let sources_by_version = BTreeMap::from([(solc, (version, sources))]);
let sources = CompilerSources::Sequential(sources_by_version);

Expand Down
146 changes: 130 additions & 16 deletions ethers-solc/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
use crate::{
artifacts::Settings,
artifacts::{output_selection::ContractOutputSelection, Settings},
cache::SOLIDITY_FILES_CACHE_FILENAME,
error::{Result, SolcError, SolcIoError},
remappings::Remapping,
resolver::{Graph, SolImportAlias},
utils, Source, Sources,
};

use crate::artifacts::output_selection::ContractOutputSelection;

use serde::{Deserialize, Serialize};
use std::{
collections::{BTreeSet, HashSet},
fmt::{self, Formatter},
fs,
ops::{Deref, DerefMut},
path::{Component, Path, PathBuf},
};

Expand Down Expand Up @@ -86,6 +84,15 @@ impl ProjectPathsConfig {
paths
}

/// Returns all `--include-path` paths that should be used for this project
///
/// See [IncludePaths]
pub fn include_paths(&self) -> Vec<PathBuf> {
// Note: root must not be included, since it will be used as base-path, which would be a
// conflict
vec![self.sources.clone(), self.tests.clone(), self.scripts.clone()]
}

/// Creates all configured dirs and files
pub fn create_all(&self) -> std::result::Result<(), SolcIoError> {
if let Some(parent) = self.cache.parent() {
Expand Down Expand Up @@ -214,11 +221,20 @@ impl ProjectPathsConfig {
/// Attempts to resolve an `import` from the given working directory.
///
/// The `cwd` path is the parent dir of the file that includes the `import`
pub fn resolve_import(&self, cwd: &Path, import: &Path) -> Result<PathBuf> {
///
/// This will also populate the `include_paths` with any nested library root paths that should
/// be provided to solc via `--include-path` because it uses absolute imports.
pub fn resolve_import_and_include_paths(
&self,
cwd: &Path,
import: &Path,
include_paths: &mut IncludePaths,
) -> Result<PathBuf> {
let component = import
.components()
.next()
.ok_or_else(|| SolcError::msg(format!("Empty import path {}", import.display())))?;

if component == Component::CurDir || component == Component::ParentDir {
// if the import is relative we assume it's already part of the processed input
// file set
Expand All @@ -227,7 +243,32 @@ impl ProjectPathsConfig {
})
} else {
// resolve library file
self.resolve_library_import(import.as_ref()).ok_or_else(|| {
let resolved = self.resolve_library_import(import.as_ref());

if resolved.is_none() {
// absolute paths in solidity are a thing for example `import
// "src/interfaces/IConfig.sol"` which could either point to `cwd +
// src/interfaces/IConfig.sol`, or make use of a remapping (`src/=....`)
if let Some(lib) = self.find_library_ancestor(cwd) {
if let Some((include_path, import)) =
utils::resolve_absolute_library(lib, cwd, import)
{
// track the path for this absolute import inside a nested library
include_paths.insert(include_path);
return Ok(import)
}
}
// also try to resolve absolute imports from the project paths
for path in [&self.root, &self.sources, &self.tests, &self.scripts] {
if cwd.starts_with(path) {
if let Ok(import) = utils::canonicalize(path.join(import)) {
return Ok(import)
}
}
}
}

resolved.ok_or_else(|| {
SolcError::msg(format!(
"failed to resolve library import \"{:?}\"",
import.display()
Expand All @@ -236,6 +277,13 @@ impl ProjectPathsConfig {
}
}

/// Attempts to resolve an `import` from the given working directory.
///
/// The `cwd` path is the parent dir of the file that includes the `import`
pub fn resolve_import(&self, cwd: &Path, import: &Path) -> Result<PathBuf> {
self.resolve_import_and_include_paths(cwd, import, &mut Default::default())
}

/// Attempts to find the path to the real solidity file that's imported via the given `import`
/// path by applying the configured remappings and checking the library dirs
///
Expand Down Expand Up @@ -751,6 +799,49 @@ impl SolcConfigBuilder {
}
}

/// Container for all `--include-path` arguments for Solc, se also [Solc docs](https://docs.soliditylang.org/en/v0.8.9/using-the-compiler.html#base-path-and-import-remapping
///
/// The `--include--path` flag:
/// > Makes an additional source directory available to the default import callback. Use this option
/// > if you want to import contracts whose location is not fixed in relation to your main source
/// > tree, e.g. third-party libraries installed using a package manager. Can be used multiple
/// > times. Can only be used if base path has a non-empty value.
///
/// In contrast to `--allow-paths` [`AllowedLibPaths`], which takes multiple arguments,
/// `--include-path` only takes a single path argument.
#[derive(Clone, Debug, Default)]
pub struct IncludePaths(pub(crate) BTreeSet<PathBuf>);

// === impl IncludePaths ===

impl IncludePaths {
/// Returns the [Command](std::process::Command) arguments for this type
///
/// For each entry in the set, it will return `--include-path` + `<entry>`
pub fn args(&self) -> impl Iterator<Item = String> + '_ {
self.paths().flat_map(|path| ["--include-path".to_string(), format!("{}", path.display())])
}

/// Returns all paths that exist
pub fn paths(&self) -> impl Iterator<Item = &PathBuf> + '_ {
self.0.iter().filter(|path| path.exists())
}
}

impl Deref for IncludePaths {
type Target = BTreeSet<PathBuf>;

fn deref(&self) -> &Self::Target {
&self.0
}
}

impl DerefMut for IncludePaths {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}

/// 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):
Expand All @@ -761,23 +852,46 @@ impl SolcConfigBuilder {
/// 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>);
pub struct AllowedLibPaths(pub(crate) BTreeSet<PathBuf>);

// === impl AllowedLibPaths ===

impl AllowedLibPaths {
pub fn is_empty(&self) -> bool {
self.0.is_empty()
/// Returns the [Command](std::process::Command) arguments for this type
///
/// `--allow-paths` takes a single value: all comma separated paths
pub fn args(&self) -> Option<[String; 2]> {
let args = self.to_string();
if args.is_empty() {
return None
}
Some(["--allow-paths".to_string(), args])
}

/// Returns all paths that exist
pub fn paths(&self) -> impl Iterator<Item = &PathBuf> + '_ {
self.0.iter().filter(|path| path.exists())
}
}

impl Deref for AllowedLibPaths {
type Target = BTreeSet<PathBuf>;

fn deref(&self) -> &Self::Target {
&self.0
}
}

impl DerefMut for AllowedLibPaths {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}

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(",");
let lib_paths =
self.paths().map(|path| format!("{}", path.display())).collect::<Vec<_>>().join(",");
write!(f, "{}", lib_paths)
}
}
Expand Down
Loading