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

* feat(solc): resolve absolute imports in libraries

* feat(solc): support --include-path

* update test

* only add base path if not empty

* simplify solc config

* include root in include paths

* test: add test for absolute imports

* fix: bad predicate

* cleanup

* fix: use base-path directly

* fix: exclude root from include set
This commit is contained in:
Matthias Seitz 2022-08-12 21:11:08 +02:00 committed by GitHub
parent 8105d8be9b
commit 2c33acb3ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 415 additions and 77 deletions

View File

@ -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/> /// <https://blog.soliditylang.org/2021/08/11/solidity-0.8.7-release-announcement/>
pub const LONDON_SOLC: Version = Version::new(0, 8, 7); 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"))] #[cfg(any(test, feature = "tests"))]
use std::sync::Mutex; use std::sync::Mutex;
@ -527,6 +535,7 @@ impl Solc {
let mut cmd = Command::new(&self.solc); let mut cmd = Command::new(&self.solc);
if let Some(ref base_path) = self.base_path { if let Some(ref base_path) = self.base_path {
cmd.current_dir(base_path); cmd.current_dir(base_path);
cmd.arg("--base-path").arg(base_path);
} }
let mut child = cmd let mut child = cmd
.args(&self.args) .args(&self.args)

View File

@ -156,8 +156,7 @@ impl<'a, T: ArtifactOutput> ProjectCompiler<'a, T> {
let graph = Graph::resolve_sources(&project.paths, sources)?; let graph = Graph::resolve_sources(&project.paths, sources)?;
let (versions, edges) = graph.into_sources_by_version(project.offline)?; let (versions, edges) = graph.into_sources_by_version(project.offline)?;
let base_path = project.root(); let sources_by_version = versions.get(project)?;
let sources_by_version = versions.get(&project.allowed_lib_paths, base_path)?;
let sources = if project.solc_jobs > 1 && sources_by_version.len() > 1 { 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 // if there are multiple different versions, and we can use multiple jobs we can compile
@ -178,6 +177,14 @@ impl<'a, T: ArtifactOutput> ProjectCompiler<'a, T> {
) -> Result<Self> { ) -> Result<Self> {
let version = solc.version()?; let version = solc.version()?;
let (sources, edges) = Graph::resolve_sources(&project.paths, sources)?.into_sources(); 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_by_version = BTreeMap::from([(solc, (version, sources))]);
let sources = CompilerSources::Sequential(sources_by_version); let sources = CompilerSources::Sequential(sources_by_version);

View File

@ -1,19 +1,17 @@
use crate::{ use crate::{
artifacts::Settings, artifacts::{output_selection::ContractOutputSelection, Settings},
cache::SOLIDITY_FILES_CACHE_FILENAME, cache::SOLIDITY_FILES_CACHE_FILENAME,
error::{Result, SolcError, SolcIoError}, error::{Result, SolcError, SolcIoError},
remappings::Remapping, remappings::Remapping,
resolver::{Graph, SolImportAlias}, resolver::{Graph, SolImportAlias},
utils, Source, Sources, utils, Source, Sources,
}; };
use crate::artifacts::output_selection::ContractOutputSelection;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{ use std::{
collections::{BTreeSet, HashSet}, collections::{BTreeSet, HashSet},
fmt::{self, Formatter}, fmt::{self, Formatter},
fs, fs,
ops::{Deref, DerefMut},
path::{Component, Path, PathBuf}, path::{Component, Path, PathBuf},
}; };
@ -86,6 +84,15 @@ impl ProjectPathsConfig {
paths 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 /// Creates all configured dirs and files
pub fn create_all(&self) -> std::result::Result<(), SolcIoError> { pub fn create_all(&self) -> std::result::Result<(), SolcIoError> {
if let Some(parent) = self.cache.parent() { if let Some(parent) = self.cache.parent() {
@ -214,11 +221,20 @@ impl ProjectPathsConfig {
/// Attempts to resolve an `import` from the given working directory. /// Attempts to resolve an `import` from the given working directory.
/// ///
/// The `cwd` path is the parent dir of the file that includes the `import` /// 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 let component = import
.components() .components()
.next() .next()
.ok_or_else(|| SolcError::msg(format!("Empty import path {}", import.display())))?; .ok_or_else(|| SolcError::msg(format!("Empty import path {}", import.display())))?;
if component == Component::CurDir || component == Component::ParentDir { if component == Component::CurDir || component == Component::ParentDir {
// if the import is relative we assume it's already part of the processed input // if the import is relative we assume it's already part of the processed input
// file set // file set
@ -227,7 +243,32 @@ impl ProjectPathsConfig {
}) })
} else { } else {
// resolve library file // 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!( SolcError::msg(format!(
"failed to resolve library import \"{:?}\"", "failed to resolve library import \"{:?}\"",
import.display() import.display()
@ -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` /// 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 /// path by applying the configured remappings and checking the library dirs
/// ///
@ -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 /// 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): /// From the [Solc docs](https://docs.soliditylang.org/en/v0.8.9/using-the-compiler.html#base-path-and-import-remapping):
@ -761,23 +852,46 @@ impl SolcConfigBuilder {
/// can be allowed via the --allow-paths /sample/path,/another/sample/path switch. /// can be allowed via the --allow-paths /sample/path,/another/sample/path switch.
/// Everything inside the path specified via --base-path is always allowed. /// Everything inside the path specified via --base-path is always allowed.
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct AllowedLibPaths(pub(crate) Vec<PathBuf>); pub struct AllowedLibPaths(pub(crate) BTreeSet<PathBuf>);
// === impl AllowedLibPaths ===
impl AllowedLibPaths { impl AllowedLibPaths {
pub fn is_empty(&self) -> bool { /// Returns the [Command](std::process::Command) arguments for this type
self.0.is_empty() ///
/// `--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 { impl fmt::Display for AllowedLibPaths {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let lib_paths = self let lib_paths =
.0 self.paths().map(|path| format!("{}", path.display())).collect::<Vec<_>>().join(",");
.iter()
.filter(|path| path.exists())
.map(|path| format!("{}", path.display()))
.collect::<Vec<_>>()
.join(",");
write!(f, "{}", lib_paths) write!(f, "{}", lib_paths)
} }
} }

View File

@ -38,6 +38,7 @@ pub use filter::{FileFilter, TestFileFilter};
use crate::{ use crate::{
artifacts::Sources, artifacts::Sources,
cache::SolFilesCache, cache::SolFilesCache,
config::IncludePaths,
error::{SolcError, SolcIoError}, error::{SolcError, SolcIoError},
sources::{VersionedSourceFile, VersionedSourceFiles}, sources::{VersionedSourceFile, VersionedSourceFiles},
}; };
@ -73,7 +74,9 @@ pub struct Project<T: ArtifactOutput = ConfigurableArtifacts> {
/// Errors/Warnings which match these error codes are not going to be logged /// Errors/Warnings which match these error codes are not going to be logged
pub ignored_error_codes: Vec<u64>, pub ignored_error_codes: Vec<u64>,
/// The paths which will be allowed for library inclusion /// The paths which will be allowed for library inclusion
pub allowed_lib_paths: AllowedLibPaths, pub allowed_paths: AllowedLibPaths,
/// The paths which will be used with solc's `--include-path` attribute
pub include_paths: IncludePaths,
/// Maximum number of `solc` processes to run simultaneously. /// Maximum number of `solc` processes to run simultaneously.
solc_jobs: usize, solc_jobs: usize,
/// Offline mode, if set, network access (download solc) is disallowed /// Offline mode, if set, network access (download solc) is disallowed
@ -153,14 +156,46 @@ impl<T: ArtifactOutput> Project<T> {
/// Applies the configured arguments to the given `Solc` /// Applies the configured arguments to the given `Solc`
/// ///
/// This will set the `--allow-paths` to the paths configured for the `Project`, if any. /// See [Self::configure_solc_with_version()]
fn configure_solc(&self, mut solc: Solc) -> Solc { pub(crate) fn configure_solc(&self, solc: Solc) -> Solc {
if !self.allowed_lib_paths.0.is_empty() && let version = solc.version().ok();
!solc.args.iter().any(|arg| arg == "--allow-paths") self.configure_solc_with_version(solc, version, Default::default())
{
solc = solc.arg("--allow-paths").arg(self.allowed_lib_paths.to_string());
} }
solc.with_base_path(self.root())
/// Applies the configured arguments to the given `Solc`
///
/// This will set the `--allow-paths` to the paths configured for the `Project`, if any.
///
/// If a version is provided and it is applicable it will also set `--base-path` and
/// `--include-path` This will set the `--allow-paths` to the paths configured for the
/// `Project`, if any.
/// This also accepts additional `include_paths`
pub(crate) fn configure_solc_with_version(
&self,
mut solc: Solc,
version: Option<Version>,
mut include_paths: IncludePaths,
) -> Solc {
if !solc.args.iter().any(|arg| arg == "--allow-paths") {
if let Some([allow, libs]) = self.allowed_paths.args() {
solc = solc.arg(allow).arg(libs);
}
}
if let Some(version) = version {
if SUPPORTS_BASE_PATH.matches(&version) {
let base_path = format!("{}", self.root().display());
if !base_path.is_empty() {
solc = solc.with_base_path(self.root());
if SUPPORTS_INCLUDE_PATH.matches(&version) {
include_paths.extend(self.include_paths.paths().cloned());
solc = solc.args(include_paths.args());
}
}
} else {
solc.base_path.take();
}
}
solc
} }
/// Sets the maximum number of parallel `solc` processes to run simultaneously. /// Sets the maximum number of parallel `solc` processes to run simultaneously.
@ -232,9 +267,7 @@ impl<T: ArtifactOutput> Project<T> {
return self.svm_compile(sources) return self.svm_compile(sources)
} }
let solc = self.configure_solc(self.solc.clone()); self.compile_with_version(&self.solc, sources)
self.compile_with_version(&solc, sources)
} }
/// Compiles a set of contracts using `svm` managed solc installs /// Compiles a set of contracts using `svm` managed solc installs
@ -360,11 +393,7 @@ impl<T: ArtifactOutput> Project<T> {
.compile() .compile()
} }
project::ProjectCompiler::with_sources_and_solc( project::ProjectCompiler::with_sources_and_solc(self, sources, self.solc.clone())?
self,
sources,
self.configure_solc(self.solc.clone()),
)?
.with_sparse_output(filter) .with_sparse_output(filter)
.compile() .compile()
} }
@ -397,12 +426,7 @@ impl<T: ArtifactOutput> Project<T> {
solc: &Solc, solc: &Solc,
sources: Sources, sources: Sources,
) -> Result<ProjectCompileOutput<T>> { ) -> Result<ProjectCompileOutput<T>> {
project::ProjectCompiler::with_sources_and_solc( project::ProjectCompiler::with_sources_and_solc(self, sources, solc.clone())?.compile()
self,
sources,
self.configure_solc(solc.clone()),
)?
.compile()
} }
/// Removes the project's artifacts and cache file /// Removes the project's artifacts and cache file
@ -541,8 +565,10 @@ pub struct ProjectBuilder<T: ArtifactOutput = ConfigurableArtifacts> {
artifacts: T, artifacts: T,
/// Which error codes to ignore /// Which error codes to ignore
pub ignored_error_codes: Vec<u64>, pub ignored_error_codes: Vec<u64>,
/// All allowed paths /// All allowed paths for solc's `--allowed-paths`
pub allowed_paths: Vec<PathBuf>, allowed_paths: AllowedLibPaths,
/// Paths to use for solc's `--include-path`
include_paths: IncludePaths,
solc_jobs: Option<usize>, solc_jobs: Option<usize>,
} }
@ -561,7 +587,8 @@ impl<T: ArtifactOutput> ProjectBuilder<T> {
slash_paths: true, slash_paths: true,
artifacts, artifacts,
ignored_error_codes: Vec::new(), ignored_error_codes: Vec::new(),
allowed_paths: vec![], allowed_paths: Default::default(),
include_paths: Default::default(),
solc_jobs: None, solc_jobs: None,
} }
} }
@ -697,6 +724,7 @@ impl<T: ArtifactOutput> ProjectBuilder<T> {
auto_detect, auto_detect,
ignored_error_codes, ignored_error_codes,
allowed_paths, allowed_paths,
include_paths,
solc_jobs, solc_jobs,
offline, offline,
build_info, build_info,
@ -715,6 +743,7 @@ impl<T: ArtifactOutput> ProjectBuilder<T> {
artifacts, artifacts,
ignored_error_codes, ignored_error_codes,
allowed_paths, allowed_paths,
include_paths,
solc_jobs, solc_jobs,
build_info, build_info,
} }
@ -723,7 +752,7 @@ impl<T: ArtifactOutput> ProjectBuilder<T> {
/// Adds an allowed-path to the solc executable /// Adds an allowed-path to the solc executable
#[must_use] #[must_use]
pub fn allowed_path<P: Into<PathBuf>>(mut self, path: P) -> Self { pub fn allowed_path<P: Into<PathBuf>>(mut self, path: P) -> Self {
self.allowed_paths.push(path.into()); self.allowed_paths.insert(path.into());
self self
} }
@ -740,6 +769,26 @@ impl<T: ArtifactOutput> ProjectBuilder<T> {
self self
} }
/// Adds an `--include-path` to the solc executable
#[must_use]
pub fn include_path<P: Into<PathBuf>>(mut self, path: P) -> Self {
self.include_paths.insert(path.into());
self
}
/// Adds multiple include-path to the solc executable
#[must_use]
pub fn include_paths<I, S>(mut self, args: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<PathBuf>,
{
for arg in args {
self = self.include_path(arg);
}
self
}
pub fn build(self) -> Result<Project<T>> { pub fn build(self) -> Result<Project<T>> {
let Self { let Self {
paths, paths,
@ -751,6 +800,7 @@ impl<T: ArtifactOutput> ProjectBuilder<T> {
artifacts, artifacts,
ignored_error_codes, ignored_error_codes,
mut allowed_paths, mut allowed_paths,
mut include_paths,
solc_jobs, solc_jobs,
offline, offline,
build_info, build_info,
@ -767,10 +817,10 @@ impl<T: ArtifactOutput> ProjectBuilder<T> {
let solc = solc.unwrap_or_default(); let solc = solc.unwrap_or_default();
let solc_config = solc_config.unwrap_or_else(|| SolcConfig::builder().build()); let solc_config = solc_config.unwrap_or_else(|| SolcConfig::builder().build());
if allowed_paths.is_empty() {
// allow every contract under root by default // allow every contract under root by default
allowed_paths.push(paths.root.clone()) allowed_paths.insert(paths.root.clone());
} // allow paths where contracts are stored by default
include_paths.extend(paths.include_paths());
Ok(Project { Ok(Project {
paths, paths,
@ -782,8 +832,9 @@ impl<T: ArtifactOutput> ProjectBuilder<T> {
auto_detect, auto_detect,
artifacts, artifacts,
ignored_error_codes, ignored_error_codes,
allowed_lib_paths: allowed_paths.into(), allowed_paths,
solc_jobs: solc_jobs.unwrap_or_else(::num_cpus::get), include_paths,
solc_jobs: solc_jobs.unwrap_or_else(num_cpus::get),
offline, offline,
slash_paths, slash_paths,
}) })

View File

@ -46,19 +46,16 @@
//! [version pragma](https://docs.soliditylang.org/en/develop/layout-of-source-files.html#version-pragma), //! [version pragma](https://docs.soliditylang.org/en/develop/layout-of-source-files.html#version-pragma),
//! which is defined on a per source file basis. //! which is defined on a per source file basis.
use crate::{error::Result, utils, IncludePaths, ProjectPathsConfig, SolcError, Source, Sources};
use parse::{SolData, SolDataUnit, SolImport};
use rayon::prelude::*;
use semver::VersionReq;
use std::{ use std::{
collections::{HashMap, HashSet, VecDeque}, collections::{HashMap, HashSet, VecDeque},
fmt, io, fmt, io,
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use parse::{SolData, SolDataUnit, SolImport};
use rayon::prelude::*;
use semver::VersionReq;
use crate::{error::Result, utils, ProjectPathsConfig, SolcError, Source, Sources};
mod parse; mod parse;
mod tree; mod tree;
@ -90,6 +87,13 @@ pub struct GraphEdges {
num_input_files: usize, num_input_files: usize,
/// tracks all imports that we failed to resolve for a file /// tracks all imports that we failed to resolve for a file
unresolved_imports: HashSet<(PathBuf, PathBuf)>, unresolved_imports: HashSet<(PathBuf, PathBuf)>,
/// tracks additional include paths resolved by scanning all imports of the graph
///
/// Absolute imports, like `import "src/Contract.sol"` are possible, but this does not play
/// nice with the standard-json import format, since the VFS won't be able to resolve
/// "src/Contract.sol" without help via `--include-path`
#[allow(unused)]
resolved_solc_include_paths: IncludePaths,
} }
impl GraphEdges { impl GraphEdges {
@ -113,6 +117,11 @@ impl GraphEdges {
self.files().skip(self.num_input_files) self.files().skip(self.num_input_files)
} }
/// Returns all additional `--include-paths`
pub fn include_paths(&self) -> &IncludePaths {
&self.resolved_solc_include_paths
}
/// Returns all imports that we failed to resolve /// Returns all imports that we failed to resolve
pub fn unresolved_imports(&self) -> &HashSet<(PathBuf, PathBuf)> { pub fn unresolved_imports(&self) -> &HashSet<(PathBuf, PathBuf)> {
&self.unresolved_imports &self.unresolved_imports
@ -333,6 +342,10 @@ impl Graph {
let mut nodes = Vec::with_capacity(unresolved.len()); let mut nodes = Vec::with_capacity(unresolved.len());
let mut edges = Vec::with_capacity(unresolved.len()); let mut edges = Vec::with_capacity(unresolved.len());
// tracks additional paths that should be used with `--include-path`, these are libraries
// that use absolute imports like `import "src/Contract.sol"`
let mut resolved_solc_include_paths = IncludePaths::default();
// keep track of all unique paths that we failed to resolve to not spam the reporter with // keep track of all unique paths that we failed to resolve to not spam the reporter with
// the same path // the same path
let mut unresolved_imports = HashSet::new(); let mut unresolved_imports = HashSet::new();
@ -349,7 +362,11 @@ impl Graph {
for import in node.data.imports.iter() { for import in node.data.imports.iter() {
let import_path = import.data().path(); let import_path = import.data().path();
match paths.resolve_import(cwd, import_path) { match paths.resolve_import_and_include_paths(
cwd,
import_path,
&mut resolved_solc_include_paths,
) {
Ok(import) => { Ok(import) => {
add_node(&mut unresolved, &mut index, &mut resolved_imports, import)?; add_node(&mut unresolved, &mut index, &mut resolved_imports, import)?;
} }
@ -391,6 +408,7 @@ impl Graph {
.collect(), .collect(),
data: Default::default(), data: Default::default(),
unresolved_imports, unresolved_imports,
resolved_solc_include_paths,
}; };
Ok(Graph { nodes, edges, root: paths.root.clone() }) Ok(Graph { nodes, edges, root: paths.root.clone() })
} }
@ -461,7 +479,14 @@ impl Graph {
} }
versioned_sources.insert(version, sources); versioned_sources.insert(version, sources);
} }
Ok((VersionedSources { inner: versioned_sources, offline }, edges)) Ok((
VersionedSources {
inner: versioned_sources,
offline,
resolved_solc_include_paths: edges.resolved_solc_include_paths.clone(),
},
edges,
))
} }
/// Writes the list of imported files into the given formatter: /// Writes the list of imported files into the given formatter:
@ -713,6 +738,7 @@ impl<'a> Iterator for NodesIter<'a> {
#[cfg(all(feature = "svm-solc"))] #[cfg(all(feature = "svm-solc"))]
#[derive(Debug)] #[derive(Debug)]
pub struct VersionedSources { pub struct VersionedSources {
resolved_solc_include_paths: IncludePaths,
inner: HashMap<crate::SolcVersion, Sources>, inner: HashMap<crate::SolcVersion, Sources>,
offline: bool, offline: bool,
} }
@ -724,17 +750,11 @@ impl VersionedSources {
/// This will also configure following solc arguments: /// This will also configure following solc arguments:
/// - `allowed_paths` /// - `allowed_paths`
/// - `base_path` /// - `base_path`
pub fn get( pub fn get<T: crate::ArtifactOutput>(
self, self,
allowed_lib_paths: &crate::AllowedLibPaths, project: &crate::Project<T>,
base_path: impl AsRef<Path>,
) -> Result<std::collections::BTreeMap<crate::Solc, (semver::Version, Sources)>> { ) -> Result<std::collections::BTreeMap<crate::Solc, (semver::Version, Sources)>> {
use crate::Solc; use crate::Solc;
// `--base-path` was introduced in 0.6.9 <https://github.com/ethereum/solidity/releases/tag/v0.6.9>
static SUPPORTS_BASE_PATH: once_cell::sync::Lazy<VersionReq> =
once_cell::sync::Lazy::new(|| VersionReq::parse(">=0.6.9").unwrap());
// we take the installer lock here to ensure installation checking is done in sync // we take the installer lock here to ensure installation checking is done in sync
#[cfg(any(test, feature = "tests"))] #[cfg(any(test, feature = "tests"))]
let _lock = crate::compile::take_solc_installer_lock(); let _lock = crate::compile::take_solc_installer_lock();
@ -771,16 +791,15 @@ impl VersionedSources {
tracing::trace!("reinstalled solc: \"{}\"", version); tracing::trace!("reinstalled solc: \"{}\"", version);
} }
} }
let mut solc = solc
.arg("--allow-paths")
.arg(allowed_lib_paths.to_string())
.with_base_path(base_path.as_ref());
let version = solc.version()?; let version = solc.version()?;
if SUPPORTS_BASE_PATH.matches(&version) { // this will configure the `Solc` executable and its arguments
solc = solc.arg("--base-path").arg(format!("{}", base_path.as_ref().display())); let solc = project.configure_solc_with_version(
} solc,
Some(version.clone()),
self.resolved_solc_include_paths.clone(),
);
sources_by_version.insert(solc, (version, sources)); sources_by_version.insert(solc, (version, sources));
} }
Ok(sources_by_version) Ok(sources_by_version)

View File

@ -223,6 +223,37 @@ pub fn resolve_library(libs: &[impl AsRef<Path>], source: impl AsRef<Path>) -> O
} }
} }
/// Tries to find an absolute import like `src/interfaces/IConfig.sol` in `cwd`, moving up the path
/// until the `root` is reached.
///
/// If an existing file under `root` is found, this returns the path up to the `import` path and the
/// canonicalized `import` path itself:
///
/// For example for following layout:
///
/// ```text
/// <root>/mydependency/
/// ├── src (`cwd`)
/// │ ├── interfaces
/// │ │ ├── IConfig.sol
/// ```
/// and `import` as `src/interfaces/IConfig.sol` and `cwd` as `src` this will return
/// (`<root>/mydependency/`, `<root>/mydependency/src/interfaces/IConfig.sol`)
pub fn resolve_absolute_library(
root: &Path,
cwd: &Path,
import: &Path,
) -> Option<(PathBuf, PathBuf)> {
let mut parent = cwd.parent()?;
while parent != root {
if let Ok(import) = canonicalize(parent.join(import)) {
return Some((parent.to_path_buf(), import))
}
parent = parent.parent()?;
}
None
}
/// Reads the list of Solc versions that have been installed in the machine. The version list is /// Reads the list of Solc versions that have been installed in the machine. The version list is
/// sorted in ascending order. /// sorted in ascending order.
/// Checks for installed solc versions under the given path as /// Checks for installed solc versions under the given path as

View File

@ -1202,7 +1202,7 @@ library MyLib {
#[test] #[test]
fn can_recompile_with_changes() { fn can_recompile_with_changes() {
let mut tmp = TempProject::dapptools().unwrap(); let mut tmp = TempProject::dapptools().unwrap();
tmp.project_mut().allowed_lib_paths = vec![tmp.root().join("modules")].into(); tmp.project_mut().allowed_paths = vec![tmp.root().join("modules")].into();
let content = r#" let content = r#"
pragma solidity ^0.8.10; pragma solidity ^0.8.10;
@ -2211,3 +2211,110 @@ fn can_add_basic_contract_and_library() {
assert!(compiled.find_first("Foo").is_some()); assert!(compiled.find_first("Foo").is_some());
assert!(compiled.find_first("Bar").is_some()); assert!(compiled.find_first("Bar").is_some());
} }
// <https://github.com/foundry-rs/foundry/issues/2706>
#[test]
fn can_handle_nested_absolute_imports() {
let mut project = TempProject::dapptools().unwrap();
let remapping = project.paths().libraries[0].join("myDepdendency");
project
.paths_mut()
.remappings
.push(Remapping::from_str(&format!("myDepdendency/={}/", remapping.display())).unwrap());
project
.add_lib(
"myDepdendency/src/interfaces/IConfig.sol",
r#"
pragma solidity ^0.8.10;
interface IConfig {}
"#,
)
.unwrap();
project
.add_lib(
"myDepdendency/src/Config.sol",
r#"
pragma solidity ^0.8.10;
import "src/interfaces/IConfig.sol";
contract Config {}
"#,
)
.unwrap();
project
.add_source(
"Greeter",
r#"
pragma solidity ^0.8.10;
import "myDepdendency/src/Config.sol";
contract Greeter {}
"#,
)
.unwrap();
let compiled = project.compile().unwrap();
assert!(!compiled.has_compiler_errors());
assert!(compiled.find_first("Greeter").is_some());
assert!(compiled.find_first("Config").is_some());
assert!(compiled.find_first("IConfig").is_some());
}
#[test]
fn can_handle_nested_test_absolute_imports() {
let project = TempProject::dapptools().unwrap();
project
.add_source(
"Contract.sol",
r#"
// SPDX-License-Identifier: UNLICENSED
pragma solidity =0.8.13;
library Library {
function f(uint256 a, uint256 b) public pure returns (uint256) {
return a + b;
}
}
contract Contract {
uint256 c;
constructor() {
c = Library.f(1, 2);
}
}
"#,
)
.unwrap();
project
.add_test(
"Contract.t.sol",
r#"
// SPDX-License-Identifier: UNLICENSED
pragma solidity =0.8.13;
import "src/Contract.sol";
contract ContractTest {
function setUp() public {
}
function test() public {
new Contract();
}
}
"#,
)
.unwrap();
let compiled = project.compile().unwrap();
assert!(!compiled.has_compiler_errors());
assert!(compiled.find_first("Contract").is_some());
}