Repository: gakonst/ethers-rs
Pull request #1540 (2022-07-31 18:45:20)

fix(solc): convert source paths on windows
<!--
Thank you for your Pull Request. Please provide a description above and review
the requirements below.

Bug fixes and new features should include tests.

Contributors guide: https://github.com/gakonst/ethers-rs/blob/master/CONTRIBUTING.md

The contributors guide includes instructions for running rustfmt and building the
documentation.
-->

## Motivation
this fixes yet another path issue on windows which can result in duplicate (`src\\File.sol` and `src/File.sol`) artifacts

Ref https://github.com/foundry-rs/foundry/runs/7586758672?check_suite_focus=true

solc uses unix style paths `/`, if it tries to resolve an import `src/File.sol` it checks the VFS for a corresponding source, `src\\File.sol` does not necessarily match so it will resolve from disk which results in duplicated entries in the output. 
<!--
Explain the context and why you're making that change. What is the problem
you're trying to solve? In some cases there is not a problem and this can be
thought of as being the motivation for your change.
-->

## Solution
convert source paths to `/` on windows before invoking solc

foundry tests: https://github.com/foundry-rs/foundry/pull/2531
<!--
Summarize the solution and provide any necessary context needed to understand
the code change.
-->

## PR Checklist

- [ ] Added Tests
- [ ] Added Documentation
- [ ] Updated the changelog


gakonst: gotta love windows. i wonder if we've been doing things the wrong way all this time, or if it's just full of footguns? how can we avoid these in the future?
mattsse: I guess the main problem is/was I've never worked with windows and just didn't know any better lol

since windows also supports `/` I think we can just convert all of them when ethers-solc spits out the Artifact set

trying to sort things out here https://github.com/foundry-rs/foundry/pull/2531 and make sure everything passes now, getting some weird failures that only happen in the cross compile tests... 
gakonst: smol lint issue otherwise merging
mattsse: lint issue is fixed here https://github.com/gakonst/ethers-rs/pull/1541

I'd like to keep this open until https://github.com/foundry-rs/foundry/pull/2531 passes

this is incredible tedious lol for some reason a random fuzzing test keeps failing randomly...
mattsse: @gakonst this is ready now, it will spit out `/` paths on windows now

Patch:
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-contract/ethers-contract-abigen/src/contract/methods.rs b/ethers-contract/ethers-contract-abigen/src/contract/methods.rs
index 3894802b..11b1641a 100644
--- a/ethers-contract/ethers-contract-abigen/src/contract/methods.rs
+++ b/ethers-contract/ethers-contract-abigen/src/contract/methods.rs
@@ -281,13 +281,9 @@ impl Context {
         util::ident(&format!("{}Calls", self.contract_ident))
     }
 
-    /// Expands to the `name : type` pairs of the function's parameters
-    fn expand_params(
-        &self,
-        fun: &Function,
-        params: &[Param],
-    ) -> Result<Vec<(TokenStream, TokenStream)>> {
-        params
+    /// Expands to the `name : type` pairs of the function's inputs
+    fn expand_input_params(&self, fun: &Function) -> Result<Vec<(TokenStream, TokenStream)>> {
+        fun.inputs
             .iter()
             .enumerate()
             .map(|(idx, param)| {
@@ -298,14 +294,17 @@ impl Context {
             .collect()
     }
 
-    /// Expands to the `name : type` pairs of the function's inputs
-    fn expand_input_params(&self, fun: &Function) -> Result<Vec<(TokenStream, TokenStream)>> {
-        self.expand_params(fun, &fun.inputs)
-    }
-
     /// Expands to the `name : type` pairs of the function's outputs
     fn expand_output_params(&self, fun: &Function) -> Result<Vec<(TokenStream, TokenStream)>> {
-        self.expand_params(fun, &fun.outputs)
+        fun.outputs
+            .iter()
+            .enumerate()
+            .map(|(idx, param)| {
+                let name = util::expand_input_name(idx, &param.name);
+                let ty = self.expand_output_param_type(fun, param, &param.kind)?;
+                Ok((name, ty))
+            })
+            .collect()
     }
 
     /// Expands to the return type of a function
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 <filename>:<name> 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<T: Serialize> Artifacts<T> {
 }
 
 impl<T> Artifacts<T> {
+    /// 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<T> {
         self.0
     }
@@ -250,7 +278,8 @@ impl<T> Artifacts<T> {
                                 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<Vec<VersionedContract>>);
 
 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<T: ArtifactOutput = ConfigurableArtifacts> {
 }
 
 impl<T: ArtifactOutput> ProjectCompileOutput<T> {
+    /// 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 `<file name>:<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<String, Vec<VersionedSourceFile>>);
 
 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<ProjectCompileOutput<T>> {
+        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<PreprocessedState<'a, T>> {
-        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<T: ArtifactOutput>(self, cache: &mut ArtifactsCache<T>) -> FilteredCompilerSources {
         fn filtered_sources<T: ArtifactOutput>(
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<T: ArtifactOutput = ConfigurableArtifacts> {
     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<T: ArtifactOutput = ConfigurableArtifacts> {
     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<T: ArtifactOutput> ProjectBuilder<T> {
             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<T: ArtifactOutput> ProjectBuilder<T> {
         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<T: ArtifactOutput> ProjectBuilder<T> {
             solc_jobs,
             offline,
             build_info,
+            slash_paths,
             ..
         } = self;
         ProjectBuilder {
@@ -694,6 +711,7 @@ impl<T: ArtifactOutput> ProjectBuilder<T> {
             no_artifacts,
             auto_detect,
             offline,
+            slash_paths,
             artifacts,
             ignored_error_codes,
             allowed_paths,
@@ -736,9 +754,15 @@ impl<T: ArtifactOutput> ProjectBuilder<T> {
             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<T: ArtifactOutput> ProjectBuilder<T> {
             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/report/mod.rs b/ethers-solc/src/report/mod.rs
index 4c7a572d..f664afca 100644
--- a/ethers-solc/src/report/mod.rs
+++ b/ethers-solc/src/report/mod.rs
@@ -134,8 +134,9 @@ pub trait Reporter: 'static + std::fmt::Debug {
     /// Invoked after a [`Solc`] installation failed
     fn on_solc_installation_error(&self, _version: &Version, _error: &str) {}
 
-    /// Invoked if the import couldn't be resolved with these remappings
-    fn on_unresolved_import(&self, _import: &Path, _remappings: &[Remapping]) {}
+    /// Invoked if imports couldn't be resolved with the given remappings, where `imports` is the
+    /// list of all import paths and the file they occurred in: `(import stmt, file)`
+    fn on_unresolved_imports(&self, _imports: &[(&Path, &Path)], _remappings: &[Remapping]) {}
 
     /// If `self` is the same type as the provided `TypeId`, returns an untyped
     /// [`NonNull`] pointer to that type. Otherwise, returns `None`.
@@ -213,8 +214,8 @@ pub(crate) fn solc_installation_error(version: &Version, error: &str) {
     get_default(|r| r.reporter.on_solc_installation_error(version, error));
 }
 
-pub(crate) fn unresolved_import(import: &Path, remappings: &[Remapping]) {
-    get_default(|r| r.reporter.on_unresolved_import(import, remappings));
+pub(crate) fn unresolved_imports(imports: &[(&Path, &Path)], remappings: &[Remapping]) {
+    get_default(|r| r.reporter.on_unresolved_imports(imports, remappings));
 }
 
 fn get_global() -> Option<&'static Report> {
@@ -388,15 +389,28 @@ impl Reporter for BasicStdoutReporter {
         eprintln!("Failed to install solc {}: {}", version, error);
     }
 
-    fn on_unresolved_import(&self, import: &Path, remappings: &[Remapping]) {
-        println!(
-            "Unable to resolve import: \"{}\" with remappings:\n        {}",
-            import.display(),
-            remappings.iter().map(|r| r.to_string()).collect::<Vec<_>>().join("\n        ")
-        );
+    fn on_unresolved_imports(&self, imports: &[(&Path, &Path)], remappings: &[Remapping]) {
+        if imports.is_empty() {
+            return
+        }
+        println!("{}", format_unresolved_imports(imports, remappings))
     }
 }
 
+/// Creates a meaningful message for all unresolved imports
+pub fn format_unresolved_imports(imports: &[(&Path, &Path)], remappings: &[Remapping]) -> String {
+    let info = imports
+        .iter()
+        .map(|(import, file)| format!("\"{}\" in \"{}\"", import.display(), file.display()))
+        .collect::<Vec<_>>()
+        .join("\n      ");
+    format!(
+        "Unable to resolve imports:\n      {}\nwith remappings:\n      {}",
+        info,
+        remappings.iter().map(|r| r.to_string()).collect::<Vec<_>>().join("\n      ")
+    )
+}
+
 /// Returned if setting the global reporter fails.
 #[derive(Debug)]
 pub struct SetGlobalReporterError {
@@ -473,6 +487,7 @@ fn set_global_reporter(report: Report) -> Result<(), SetGlobalReporterError> {
 #[cfg(test)]
 mod tests {
     use super::*;
+    use std::str::FromStr;
 
     #[test]
     fn scoped_reporter_works() {
@@ -502,4 +517,21 @@ mod tests {
 
         get_default(|reporter| assert!(reporter.is::<BasicStdoutReporter>()))
     }
+
+    #[test]
+    fn test_unresolved_message() {
+        let unresolved = vec![(Path::new("./src/Import.sol"), Path::new("src/File.col"))];
+
+        let remappings = vec![Remapping::from_str("oz=a/b/c/d").unwrap()];
+
+        assert_eq!(
+            format_unresolved_imports(&unresolved, &remappings).trim(),
+            r#"
+Unable to resolve imports:
+      "./src/Import.sol" in "src/File.col"
+with remappings:
+      oz=a/b/c/d/"#
+                .trim()
+        )
+    }
 }
diff --git a/ethers-solc/src/resolver/mod.rs b/ethers-solc/src/resolver/mod.rs
index 571e3602..374f678e 100644
--- a/ethers-solc/src/resolver/mod.rs
+++ b/ethers-solc/src/resolver/mod.rs
@@ -88,8 +88,8 @@ pub struct GraphEdges {
     /// Combined with the `indices` this way we can determine if a file was original added to the
     /// graph as input or was added as resolved import, see [`Self::is_input_file()`]
     num_input_files: usize,
-    /// tracks all imports that we failed to resolve
-    unresolved_imports: HashSet<PathBuf>,
+    /// tracks all imports that we failed to resolve for a file
+    unresolved_imports: HashSet<(PathBuf, PathBuf)>,
 }
 
 impl GraphEdges {
@@ -114,7 +114,7 @@ impl GraphEdges {
     }
 
     /// Returns all imports that we failed to resolve
-    pub fn unresolved_imports(&self) -> &HashSet<PathBuf> {
+    pub fn unresolved_imports(&self) -> &HashSet<(PathBuf, PathBuf)> {
         &self.unresolved_imports
     }
 
@@ -354,9 +354,7 @@ impl Graph {
                         add_node(&mut unresolved, &mut index, &mut resolved_imports, import)?;
                     }
                     Err(err) => {
-                        if unresolved_imports.insert(import_path.to_path_buf()) {
-                            crate::report::unresolved_import(import_path, &paths.remappings);
-                        }
+                        unresolved_imports.insert((import_path.to_path_buf(), node.path.clone()));
                         tracing::trace!(
                             "failed to resolve import component \"{:?}\" for {:?}",
                             err,
@@ -369,6 +367,18 @@ impl Graph {
             nodes.push(node);
             edges.push(resolved_imports);
         }
+
+        if !unresolved_imports.is_empty() {
+            // notify on all unresolved imports
+            crate::report::unresolved_imports(
+                &unresolved_imports
+                    .iter()
+                    .map(|(i, f)| (i.as_path(), f.as_path()))
+                    .collect::<Vec<_>>(),
+                &paths.remappings,
+            );
+        }
+
         let edges = GraphEdges {
             edges,
             rev_indices: index.iter().map(|(k, v)| (*v, k.clone())).collect(),
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<Path>], source: impl AsRef<Path>)
 }
 
 /// Canonicalize the path, platform-agnostic
+///
+/// On windows this will ensure the path only consists of `/` separators
 pub fn canonicalize(path: impl AsRef<Path>) -> Result<PathBuf, SolcIoError> {
     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.


Test patch:
diff --git a/ethers-contract/tests/it/abigen.rs b/ethers-contract/tests/it/abigen.rs
index fd661ad0..9ca7e9a9 100644
--- a/ethers-contract/tests/it/abigen.rs
+++ b/ethers-contract/tests/it/abigen.rs
@@ -659,3 +659,10 @@ fn can_generate_to_string_overload() {
 fn can_generate_large_event() {
     abigen!(NewSale, "ethers-contract/tests/solidity-contracts/sale.json");
 }
+
+#[test]
+fn can_generate_large_output_struct() {
+    abigen!(LargeOutputStruct, "ethers-contract/tests/solidity-contracts/LargeStruct.json");
+
+    let r = GetByIdReturn(Info::default());
+}
diff --git a/ethers-contract/tests/solidity-contracts/LargeStruct.json b/ethers-contract/tests/solidity-contracts/LargeStruct.json
new file mode 100644
index 00000000..04e4531b
--- /dev/null
+++ b/ethers-contract/tests/solidity-contracts/LargeStruct.json
@@ -0,0 +1,68 @@
+[
+  {
+    "inputs": [
+      {
+        "internalType": "bytes32",
+        "name": "id",
+        "type": "bytes32"
+      }
+    ],
+    "name": "getById",
+    "outputs": [
+      {
+        "components": [
+          {
+            "internalType": "uint128",
+            "name": "x",
+            "type": "uint128"
+          },
+          {
+            "internalType": "int24",
+            "name": "y",
+            "type": "int24"
+          },
+          {
+            "internalType": "int24",
+            "name": "z",
+            "type": "int24"
+          },
+          {
+            "internalType": "uint256",
+            "name": "a",
+            "type": "uint256"
+          },
+          {
+            "internalType": "int256",
+            "name": "b",
+            "type": "int256"
+          },
+          {
+            "internalType": "int256",
+            "name": "c",
+            "type": "int256"
+          },
+          {
+            "internalType": "int256",
+            "name": "d",
+            "type": "int256"
+          },
+          {
+            "internalType": "uint256",
+            "name": "e",
+            "type": "uint256"
+          },
+          {
+            "internalType": "uint256",
+            "name": "f",
+            "type": "uint256"
+          }
+        ],
+        "internalType": "struct Many.Info",
+        "name": "",
+        "type": "tuple"
+      }
+    ],
+    "stateMutability": "view",
+    "type": "function"
+  }
+]
\ No newline at end of file