diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f0b5607..bd3c3941 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -115,6 +115,8 @@ ### Unreleased +- On windows all paths in the `ProjectCompilerOutput` are now slashed by default + [#1540](https://github.com/gakonst/ethers-rs/pull/1540) - `ArtifactOutput::write_extras` now takes the `Artifacts` directly [#1491](https://github.com/gakonst/ethers-rs/pull/1491) - Make `ethers-solc` optional dependency of `ethers`, needs `ethers-solc` feature to activate diff --git a/ethers-solc/src/artifact_output/mod.rs b/ethers-solc/src/artifact_output/mod.rs index 9ca5dcf3..11759fae 100644 --- a/ethers-solc/src/artifact_output/mod.rs +++ b/ethers-solc/src/artifact_output/mod.rs @@ -42,6 +42,22 @@ pub struct ArtifactId { } impl ArtifactId { + /// Converts any `\\` separators in the `path` to `/` + pub fn slash_paths(&mut self) { + #[cfg(windows)] + { + use path_slash::PathBufExt; + self.path = self.path.to_slash_lossy().as_ref().into(); + self.source = self.source.to_slash_lossy().as_ref().into(); + } + } + + /// Convenience function fo [`Self::slash_paths()`] + pub fn with_slashed_paths(mut self) -> Self { + self.slash_paths(); + self + } + /// Returns a : slug that identifies an artifact /// /// Note: This identifier is not necessarily unique. If two contracts have the same name, they @@ -178,6 +194,18 @@ impl Artifacts { } impl Artifacts { + /// Converts all `\\` separators in _all_ paths to `/` + pub fn slash_paths(&mut self) { + #[cfg(windows)] + { + use path_slash::PathExt; + self.0 = std::mem::take(&mut self.0) + .into_iter() + .map(|(path, files)| (Path::new(&path).to_slash_lossy().to_string(), files)) + .collect() + } + } + pub fn into_inner(self) -> ArtifactsMap { self.0 } @@ -250,7 +278,8 @@ impl Artifacts { name, source: source.clone(), version: artifact.version, - }, + } + .with_slashed_paths(), artifact.artifact, ) }) diff --git a/ethers-solc/src/compile/output/contracts.rs b/ethers-solc/src/compile/output/contracts.rs index 7e17a15a..09aab711 100644 --- a/ethers-solc/src/compile/output/contracts.rs +++ b/ethers-solc/src/compile/output/contracts.rs @@ -15,6 +15,17 @@ use std::{collections::BTreeMap, ops::Deref, path::Path}; pub struct VersionedContracts(pub FileToContractsMap>); impl VersionedContracts { + /// Converts all `\\` separators in _all_ paths to `/` + pub fn slash_paths(&mut self) { + #[cfg(windows)] + { + use path_slash::PathExt; + self.0 = std::mem::take(&mut self.0) + .into_iter() + .map(|(path, files)| (Path::new(&path).to_slash_lossy().to_string(), files)) + .collect() + } + } pub fn is_empty(&self) -> bool { self.0.is_empty() } diff --git a/ethers-solc/src/compile/output/mod.rs b/ethers-solc/src/compile/output/mod.rs index 298a0680..bddb0890 100644 --- a/ethers-solc/src/compile/output/mod.rs +++ b/ethers-solc/src/compile/output/mod.rs @@ -34,6 +34,19 @@ pub struct ProjectCompileOutput { } impl ProjectCompileOutput { + /// Converts all `\\` separators in _all_ paths to `/` + pub fn slash_paths(&mut self) { + self.compiler_output.slash_paths(); + self.compiled_artifacts.slash_paths(); + self.cached_artifacts.slash_paths(); + } + + /// Convenience function fo [`Self::slash_paths()`] + pub fn with_slashed_paths(mut self) -> Self { + self.slash_paths(); + self + } + /// All artifacts together with their contract file name and name `:` /// /// This returns a chained iterator of both cached and recompiled contract artifacts @@ -386,6 +399,12 @@ pub struct AggregatedCompilerOutput { } impl AggregatedCompilerOutput { + /// Converts all `\\` separators in _all_ paths to `/` + pub fn slash_paths(&mut self) { + self.sources.slash_paths(); + self.contracts.slash_paths(); + } + /// Whether the output contains a compiler error pub fn has_error(&self) -> bool { self.errors.iter().any(|err| err.severity.is_error()) diff --git a/ethers-solc/src/compile/output/sources.rs b/ethers-solc/src/compile/output/sources.rs index b1f52a53..32e0ed31 100644 --- a/ethers-solc/src/compile/output/sources.rs +++ b/ethers-solc/src/compile/output/sources.rs @@ -9,6 +9,18 @@ use std::{collections::BTreeMap, path::Path}; pub struct VersionedSourceFiles(pub BTreeMap>); impl VersionedSourceFiles { + /// Converts all `\\` separators in _all_ paths to `/` + pub fn slash_paths(&mut self) { + #[cfg(windows)] + { + use path_slash::PathExt; + self.0 = std::mem::take(&mut self.0) + .into_iter() + .map(|(path, files)| (Path::new(&path).to_slash_lossy().to_string(), files)) + .collect() + } + } + pub fn is_empty(&self) -> bool { self.0.is_empty() } diff --git a/ethers-solc/src/compile/project.rs b/ethers-solc/src/compile/project.rs index 5c6e8769..5275efbe 100644 --- a/ethers-solc/src/compile/project.rs +++ b/ethers-solc/src/compile/project.rs @@ -207,15 +207,28 @@ impl<'a, T: ArtifactOutput> ProjectCompiler<'a, T> { /// let output = project.compile().unwrap(); /// ``` pub fn compile(self) -> Result> { + let slash_paths = self.project.slash_paths; + // drive the compiler statemachine to completion - self.preprocess()?.compile()?.write_artifacts()?.write_cache() + let mut output = self.preprocess()?.compile()?.write_artifacts()?.write_cache()?; + + if slash_paths { + // ensures we always use `/` paths + output.slash_paths(); + } + + Ok(output) } /// Does basic preprocessing /// - sets proper source unit names /// - check cache fn preprocess(self) -> Result> { - let Self { edges, project, sources, sparse_output } = self; + let Self { edges, project, mut sources, sparse_output } = self; + + // convert paths on windows to ensure consistency with the `CompilerOutput` `solc` emits, + // which is unix style `/` + sources.slash_paths(); let mut cache = ArtifactsCache::new(project, edges)?; // retain and compile only dirty sources and all their imports @@ -344,6 +357,32 @@ enum CompilerSources { } impl CompilerSources { + /// Converts all `\\` separators to `/` + /// + /// This effectively ensures that `solc` can find imported files like `/src/Cheats.sol` in the + /// VFS (the `CompilerInput` as json) under `src/Cheats.sol`. + fn slash_paths(&mut self) { + #[cfg(windows)] + { + use path_slash::PathBufExt; + + fn slash_versioned_sources(v: &mut VersionedSources) { + for (_, (_, sources)) in v { + *sources = std::mem::take(sources) + .into_iter() + .map(|(path, source)| { + (PathBuf::from(path.to_slash_lossy().as_ref()), source) + }) + .collect() + } + } + + match self { + CompilerSources::Sequential(v) => slash_versioned_sources(v), + CompilerSources::Parallel(v, _) => slash_versioned_sources(v), + }; + } + } /// Filters out all sources that don't need to be compiled, see [`ArtifactsCache::filter`] fn filtered(self, cache: &mut ArtifactsCache) -> FilteredCompilerSources { fn filtered_sources( diff --git a/ethers-solc/src/config.rs b/ethers-solc/src/config.rs index 75dc4d9a..2e7b91d6 100644 --- a/ethers-solc/src/config.rs +++ b/ethers-solc/src/config.rs @@ -8,6 +8,7 @@ use crate::{ }; use crate::artifacts::output_selection::ContractOutputSelection; + use serde::{Deserialize, Serialize}; use std::{ collections::{BTreeSet, HashSet}, @@ -145,6 +146,28 @@ impl ProjectPathsConfig { Ok(Source::read_all_files(self.input_files())?) } + /// Converts all `\\` separators in _all_ paths to `/` + pub fn slash_paths(&mut self) { + #[cfg(windows)] + { + use path_slash::PathBufExt; + + let slashed = |p: &mut PathBuf| { + *p = p.to_slash_lossy().as_ref().into(); + }; + slashed(&mut self.root); + slashed(&mut self.cache); + slashed(&mut self.artifacts); + slashed(&mut self.build_infos); + slashed(&mut self.sources); + slashed(&mut self.tests); + slashed(&mut self.scripts); + + self.libraries.iter_mut().for_each(slashed); + self.remappings.iter_mut().for_each(Remapping::slash_path); + } + } + /// Attempts to resolve an `import` from the given working directory. /// /// The `cwd` path is the parent dir of the file that includes the `import` diff --git a/ethers-solc/src/lib.rs b/ethers-solc/src/lib.rs index ee72845b..0e39c8a7 100644 --- a/ethers-solc/src/lib.rs +++ b/ethers-solc/src/lib.rs @@ -78,6 +78,10 @@ pub struct Project { solc_jobs: usize, /// Offline mode, if set, network access (download solc) is disallowed pub offline: bool, + /// Windows only config value to ensure the all paths use `/` instead of `\\`, same as `solc` + /// + /// This is a noop on other platforms + pub slash_paths: bool, } impl Project { @@ -531,6 +535,8 @@ pub struct ProjectBuilder { auto_detect: bool, /// Use offline mode offline: bool, + /// Whether to slash paths of the `ProjectCompilerOutput` + slash_paths: bool, /// handles all artifacts related tasks artifacts: T, /// Which error codes to ignore @@ -552,6 +558,7 @@ impl ProjectBuilder { no_artifacts: false, auto_detect: true, offline: false, + slash_paths: true, artifacts, ignored_error_codes: Vec::new(), allowed_paths: vec![], @@ -626,6 +633,15 @@ impl ProjectBuilder { self } + /// Sets whether to slash all paths on windows + /// + /// If set to `true` all `\\` separators are replaced with `/`, same as solc + #[must_use] + pub fn set_slashed_paths(mut self, slashed_paths: bool) -> Self { + self.slash_paths = slashed_paths; + self + } + /// Disables writing artifacts to disk #[must_use] pub fn no_artifacts(self) -> Self { @@ -684,6 +700,7 @@ impl ProjectBuilder { solc_jobs, offline, build_info, + slash_paths, .. } = self; ProjectBuilder { @@ -694,6 +711,7 @@ impl ProjectBuilder { no_artifacts, auto_detect, offline, + slash_paths, artifacts, ignored_error_codes, allowed_paths, @@ -736,9 +754,15 @@ impl ProjectBuilder { solc_jobs, offline, build_info, + slash_paths, } = self; - let paths = paths.map(Ok).unwrap_or_else(ProjectPathsConfig::current_hardhat)?; + let mut paths = paths.map(Ok).unwrap_or_else(ProjectPathsConfig::current_hardhat)?; + + if slash_paths { + // ensures we always use `/` paths + paths.slash_paths(); + } let solc = solc.unwrap_or_default(); let solc_config = solc_config.unwrap_or_else(|| SolcConfig::builder().build()); @@ -761,6 +785,7 @@ impl ProjectBuilder { allowed_lib_paths: allowed_paths.into(), solc_jobs: solc_jobs.unwrap_or_else(::num_cpus::get), offline, + slash_paths, }) } } diff --git a/ethers-solc/src/remappings.rs b/ethers-solc/src/remappings.rs index b63f564c..cbeea7b0 100644 --- a/ethers-solc/src/remappings.rs +++ b/ethers-solc/src/remappings.rs @@ -1,4 +1,5 @@ use crate::utils; + use serde::{Deserialize, Serialize}; use std::{ collections::{hash_map::Entry, HashMap}, @@ -224,6 +225,15 @@ impl Remapping { .map(|(name, path)| Remapping { name, path: format!("{}/", path.display()) }) .collect() } + + /// Converts any `\\` separators in the `path` to `/` + pub fn slash_path(&mut self) { + #[cfg(windows)] + { + use path_slash::PathExt; + self.path = Path::new(&self.path).to_slash_lossy().to_string(); + } + } } /// A relative [`Remapping`] that's aware of the current location @@ -263,7 +273,7 @@ impl RelativeRemapping { impl fmt::Display for RelativeRemapping { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut s = { - #[cfg(target_os = "windows")] + #[cfg(windows)] { // ensure we have `/` slashes on windows use path_slash::PathExt; diff --git a/ethers-solc/src/utils.rs b/ethers-solc/src/utils.rs index 487055cf..a14e6d27 100644 --- a/ethers-solc/src/utils.rs +++ b/ethers-solc/src/utils.rs @@ -1,5 +1,6 @@ //! Utility functions +use cfg_if::cfg_if; use std::{ collections::HashSet, ops::Range, @@ -157,9 +158,22 @@ pub fn is_local_source_name(libs: &[impl AsRef], source: impl AsRef) } /// Canonicalize the path, platform-agnostic +/// +/// On windows this will ensure the path only consists of `/` separators pub fn canonicalize(path: impl AsRef) -> Result { let path = path.as_ref(); - dunce::canonicalize(&path).map_err(|err| SolcIoError::new(err, path)) + cfg_if! { + if #[cfg(windows)] { + let res = dunce::canonicalize(path).map(|p| { + use path_slash::PathBufExt; + PathBuf::from(p.to_slash_lossy().as_ref()) + }); + } else { + let res = dunce::canonicalize(path); + } + }; + + res.map_err(|err| SolcIoError::new(err, path)) } /// Returns the same path config but with canonicalized paths.