ethers_solc/
config.rs

1use crate::{
2    artifacts::{output_selection::ContractOutputSelection, Settings},
3    cache::SOLIDITY_FILES_CACHE_FILENAME,
4    error::{Result, SolcError, SolcIoError},
5    remappings::Remapping,
6    resolver::{Graph, SolImportAlias},
7    utils, Source, Sources,
8};
9use serde::{Deserialize, Serialize};
10use std::{
11    collections::{BTreeSet, HashSet},
12    fmt::{self, Formatter},
13    fs,
14    ops::{Deref, DerefMut},
15    path::{Component, Path, PathBuf},
16};
17
18/// Where to find all files or where to write them
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct ProjectPathsConfig {
21    /// Project root
22    pub root: PathBuf,
23    /// Path to the cache, if any
24    pub cache: PathBuf,
25    /// Where to store build artifacts
26    pub artifacts: PathBuf,
27    /// Where to store the build info files
28    pub build_infos: PathBuf,
29    /// Where to find sources
30    pub sources: PathBuf,
31    /// Where to find tests
32    pub tests: PathBuf,
33    /// Where to find scripts
34    pub scripts: PathBuf,
35    /// Where to look for libraries
36    pub libraries: Vec<PathBuf>,
37    /// The compiler remappings
38    pub remappings: Vec<Remapping>,
39}
40
41impl ProjectPathsConfig {
42    pub fn builder() -> ProjectPathsConfigBuilder {
43        ProjectPathsConfigBuilder::default()
44    }
45
46    /// Creates a new hardhat style config instance which points to the canonicalized root path
47    pub fn hardhat(root: impl AsRef<Path>) -> Result<Self> {
48        PathStyle::HardHat.paths(root)
49    }
50
51    /// Creates a new dapptools style config instance which points to the canonicalized root path
52    pub fn dapptools(root: impl AsRef<Path>) -> Result<Self> {
53        PathStyle::Dapptools.paths(root)
54    }
55
56    /// Creates a new config with the current directory as the root
57    pub fn current_hardhat() -> Result<Self> {
58        Self::hardhat(std::env::current_dir().map_err(|err| SolcError::io(err, "."))?)
59    }
60
61    /// Creates a new config with the current directory as the root
62    pub fn current_dapptools() -> Result<Self> {
63        Self::dapptools(std::env::current_dir().map_err(|err| SolcError::io(err, "."))?)
64    }
65
66    /// Returns a new [ProjectPaths] instance that contains all directories configured for this
67    /// project
68    pub fn paths(&self) -> ProjectPaths {
69        ProjectPaths {
70            artifacts: self.artifacts.clone(),
71            build_infos: self.build_infos.clone(),
72            sources: self.sources.clone(),
73            tests: self.tests.clone(),
74            scripts: self.scripts.clone(),
75            libraries: self.libraries.iter().cloned().collect(),
76        }
77    }
78
79    /// Same as [`paths`][ProjectPathsConfig::paths] but strips the `root` form all paths.
80    ///
81    /// See: [`ProjectPaths::strip_prefix_all`]
82    pub fn paths_relative(&self) -> ProjectPaths {
83        let mut paths = self.paths();
84        paths.strip_prefix_all(&self.root);
85        paths
86    }
87
88    /// Creates all configured dirs and files
89    pub fn create_all(&self) -> std::result::Result<(), SolcIoError> {
90        if let Some(parent) = self.cache.parent() {
91            fs::create_dir_all(parent).map_err(|err| SolcIoError::new(err, parent))?;
92        }
93        fs::create_dir_all(&self.artifacts)
94            .map_err(|err| SolcIoError::new(err, &self.artifacts))?;
95        fs::create_dir_all(&self.sources).map_err(|err| SolcIoError::new(err, &self.sources))?;
96        fs::create_dir_all(&self.tests).map_err(|err| SolcIoError::new(err, &self.tests))?;
97        fs::create_dir_all(&self.scripts).map_err(|err| SolcIoError::new(err, &self.scripts))?;
98        for lib in &self.libraries {
99            fs::create_dir_all(lib).map_err(|err| SolcIoError::new(err, lib))?;
100        }
101        Ok(())
102    }
103
104    /// Returns all sources found under the project's configured `sources` path
105    pub fn read_sources(&self) -> Result<Sources> {
106        tracing::trace!("reading all sources from \"{}\"", self.sources.display());
107        Ok(Source::read_all_from(&self.sources)?)
108    }
109
110    /// Returns all sources found under the project's configured `test` path
111    pub fn read_tests(&self) -> Result<Sources> {
112        tracing::trace!("reading all tests from \"{}\"", self.tests.display());
113        Ok(Source::read_all_from(&self.tests)?)
114    }
115
116    /// Returns all sources found under the project's configured `script` path
117    pub fn read_scripts(&self) -> Result<Sources> {
118        tracing::trace!("reading all scripts from \"{}\"", self.scripts.display());
119        Ok(Source::read_all_from(&self.scripts)?)
120    }
121
122    /// Returns true if the there is at least one solidity file in this config.
123    ///
124    /// See also, `Self::input_files()`
125    pub fn has_input_files(&self) -> bool {
126        self.input_files_iter().next().is_some()
127    }
128
129    /// Returns an iterator that yields all solidity file paths for `Self::sources`, `Self::tests`
130    /// and `Self::scripts`
131    pub fn input_files_iter(&self) -> impl Iterator<Item = PathBuf> + '_ {
132        utils::source_files_iter(&self.sources)
133            .chain(utils::source_files_iter(&self.tests))
134            .chain(utils::source_files_iter(&self.scripts))
135    }
136
137    /// Returns the combined set solidity file paths for `Self::sources`, `Self::tests` and
138    /// `Self::scripts`
139    pub fn input_files(&self) -> Vec<PathBuf> {
140        self.input_files_iter().collect()
141    }
142
143    /// Returns the combined set of `Self::read_sources` + `Self::read_tests` + `Self::read_scripts`
144    pub fn read_input_files(&self) -> Result<Sources> {
145        Ok(Source::read_all_files(self.input_files())?)
146    }
147
148    /// Converts all `\\` separators in _all_ paths to `/`
149    pub fn slash_paths(&mut self) {
150        #[cfg(windows)]
151        {
152            use path_slash::PathBufExt;
153
154            let slashed = |p: &mut PathBuf| {
155                *p = p.to_slash_lossy().as_ref().into();
156            };
157            slashed(&mut self.root);
158            slashed(&mut self.cache);
159            slashed(&mut self.artifacts);
160            slashed(&mut self.build_infos);
161            slashed(&mut self.sources);
162            slashed(&mut self.tests);
163            slashed(&mut self.scripts);
164
165            self.libraries.iter_mut().for_each(slashed);
166            self.remappings.iter_mut().for_each(Remapping::slash_path);
167        }
168    }
169
170    /// Returns true if the `file` belongs to a `library`, See [`Self::find_library_ancestor()`]
171    pub fn has_library_ancestor(&self, file: impl AsRef<Path>) -> bool {
172        self.find_library_ancestor(file).is_some()
173    }
174
175    /// Returns the library the file belongs to
176    ///
177    /// Returns the first library that is an ancestor of the given `file`.
178    ///
179    /// **Note:** this does not resolve remappings [`Self::resolve_import()`], instead this merely
180    /// checks if a `library` is a parent of `file`
181    ///
182    /// # Example
183    ///
184    /// ```
185    ///  use std::path::Path;
186    /// use ethers_solc::ProjectPathsConfig;
187    /// let config = ProjectPathsConfig::builder().lib("lib").build().unwrap();
188    /// assert_eq!(config.find_library_ancestor("lib/src/Greeter.sol").unwrap(), Path::new("lib"));
189    /// ```
190    pub fn find_library_ancestor(&self, file: impl AsRef<Path>) -> Option<&PathBuf> {
191        let file = file.as_ref();
192
193        for lib in &self.libraries {
194            if lib.is_relative() &&
195                file.is_absolute() &&
196                file.starts_with(&self.root) &&
197                file.starts_with(self.root.join(lib)) ||
198                file.is_relative() &&
199                    lib.is_absolute() &&
200                    lib.starts_with(&self.root) &&
201                    self.root.join(file).starts_with(lib)
202            {
203                return Some(lib)
204            }
205            if file.starts_with(lib) {
206                return Some(lib)
207            }
208        }
209
210        None
211    }
212
213    /// Attempts to resolve an `import` from the given working directory.
214    ///
215    /// The `cwd` path is the parent dir of the file that includes the `import`
216    ///
217    /// This will also populate the `include_paths` with any nested library root paths that should
218    /// be provided to solc via `--include-path` because it uses absolute imports.
219    pub fn resolve_import_and_include_paths(
220        &self,
221        cwd: &Path,
222        import: &Path,
223        include_paths: &mut IncludePaths,
224    ) -> Result<PathBuf> {
225        let component = import
226            .components()
227            .next()
228            .ok_or_else(|| SolcError::msg(format!("Empty import path {}", import.display())))?;
229
230        if component == Component::CurDir || component == Component::ParentDir {
231            // if the import is relative we assume it's already part of the processed input
232            // file set
233            utils::canonicalize(cwd.join(import)).map_err(|err| {
234                SolcError::msg(format!("failed to resolve relative import \"{err:?}\""))
235            })
236        } else {
237            // resolve library file
238            let resolved = self.resolve_library_import(cwd.as_ref(), import.as_ref());
239
240            if resolved.is_none() {
241                // absolute paths in solidity are a thing for example `import
242                // "src/interfaces/IConfig.sol"` which could either point to `cwd +
243                // src/interfaces/IConfig.sol`, or make use of a remapping (`src/=....`)
244                if let Some(lib) = self.find_library_ancestor(cwd) {
245                    if let Some((include_path, import)) =
246                        utils::resolve_absolute_library(lib, cwd, import)
247                    {
248                        // track the path for this absolute import inside a nested library
249                        include_paths.insert(include_path);
250                        return Ok(import)
251                    }
252                }
253                // also try to resolve absolute imports from the project paths
254                for path in [&self.root, &self.sources, &self.tests, &self.scripts] {
255                    if cwd.starts_with(path) {
256                        if let Ok(import) = utils::canonicalize(path.join(import)) {
257                            return Ok(import)
258                        }
259                    }
260                }
261            }
262
263            resolved.ok_or_else(|| {
264                SolcError::msg(format!(
265                    "failed to resolve library import \"{:?}\"",
266                    import.display()
267                ))
268            })
269        }
270    }
271
272    /// Attempts to resolve an `import` from the given working directory.
273    ///
274    /// The `cwd` path is the parent dir of the file that includes the `import`
275    pub fn resolve_import(&self, cwd: &Path, import: &Path) -> Result<PathBuf> {
276        self.resolve_import_and_include_paths(cwd, import, &mut Default::default())
277    }
278
279    /// Attempts to find the path to the real solidity file that's imported via the given `import`
280    /// path by applying the configured remappings and checking the library dirs
281    ///
282    /// # Example
283    ///
284    /// Following `@aave` dependency in the `lib` folder `node_modules`
285    ///
286    /// ```text
287    /// <root>/node_modules/@aave
288    /// ├── aave-token
289    /// │   ├── contracts
290    /// │   │   ├── open-zeppelin
291    /// │   │   ├── token
292    /// ├── governance-v2
293    ///     ├── contracts
294    ///         ├── interfaces
295    /// ```
296    ///
297    /// has this remapping: `@aave/=@aave/` (name:path) so contracts can be imported as
298    ///
299    /// ```solidity
300    /// import "@aave/governance-v2/contracts/governance/Executor.sol";
301    /// ```
302    ///
303    /// So that `Executor.sol` can be found by checking each `lib` folder (`node_modules`) with
304    /// applied remappings. Applying remapping works by checking if the import path of an import
305    /// statement starts with the name of a remapping and replacing it with the remapping's `path`.
306    ///
307    /// There are some caveats though, dapptools style remappings usually include the `src` folder
308    /// `ds-test/=lib/ds-test/src/` so that imports look like `import "ds-test/test.sol";` (note the
309    /// missing `src` in the import path).
310    ///
311    /// For hardhat/npm style that's not always the case, most notably for [openzeppelin-contracts](https://github.com/OpenZeppelin/openzeppelin-contracts) if installed via npm.
312    /// The remapping is detected as `'@openzeppelin/=node_modules/@openzeppelin/contracts/'`, which
313    /// includes the source directory `contracts`, however it's common to see import paths like:
314    ///
315    /// `import "@openzeppelin/contracts/token/ERC20/IERC20.sol";`
316    ///
317    /// instead of
318    ///
319    /// `import "@openzeppelin/token/ERC20/IERC20.sol";`
320    ///
321    /// There is no strict rule behind this, but because [`crate::remappings::Remapping::find_many`]
322    /// returns `'@openzeppelin/=node_modules/@openzeppelin/contracts/'` we should handle the
323    /// case if the remapping path ends with `/contracts/` and the import path starts with
324    /// `<remapping name>/contracts/`. Otherwise we can end up with a resolved path that has a
325    /// duplicate `contracts` segment:
326    /// `@openzeppelin/contracts/contracts/token/ERC20/IERC20.sol` we check for this edge case
327    /// here so that both styles work out of the box.
328    pub fn resolve_library_import(&self, cwd: &Path, import: &Path) -> Option<PathBuf> {
329        // if the import path starts with the name of the remapping then we get the resolved path by
330        // removing the name and adding the remainder to the path of the remapping
331        let cwd = cwd.strip_prefix(&self.root).unwrap_or(cwd);
332        if let Some(path) = self
333            .remappings
334            .iter()
335            .filter(|r| {
336                // only check remappings that are either global or for `cwd`
337                if let Some(ctx) = r.context.as_ref() {
338                    cwd.starts_with(ctx)
339                } else {
340                    true
341                }
342            })
343            .find_map(|r| {
344                import.strip_prefix(&r.name).ok().map(|stripped_import| {
345                    let lib_path = Path::new(&r.path).join(stripped_import);
346
347                    // we handle the edge case where the path of a remapping ends with "/contracts/"
348                    // (`<name>/=.../contracts/`) and the stripped import also starts with
349                    // `contracts/`
350                    if let Ok(adjusted_import) = stripped_import.strip_prefix("contracts/") {
351                        // Wrap suffix check in `/` so this prevents matching remapping paths that
352                        // end with '-contracts/', '_contracts/', '.contracts/' etc.
353                        if r.path.ends_with("/contracts/") && !lib_path.exists() {
354                            return Path::new(&r.path).join(adjusted_import)
355                        }
356                    }
357                    lib_path
358                })
359            })
360        {
361            Some(self.root.join(path))
362        } else {
363            utils::resolve_library(&self.libraries, import)
364        }
365    }
366
367    /// Attempts to autodetect the artifacts directory based on the given root path
368    ///
369    /// Dapptools layout takes precedence over hardhat style.
370    /// This will return:
371    ///   - `<root>/out` if it exists or `<root>/artifacts` does not exist,
372    ///   - `<root>/artifacts` if it exists and `<root>/out` does not exist.
373    pub fn find_artifacts_dir(root: impl AsRef<Path>) -> PathBuf {
374        utils::find_fave_or_alt_path(root, "out", "artifacts")
375    }
376
377    /// Attempts to autodetect the source directory based on the given root path
378    ///
379    /// Dapptools layout takes precedence over hardhat style.
380    /// This will return:
381    ///   - `<root>/src` if it exists or `<root>/contracts` does not exist,
382    ///   - `<root>/contracts` if it exists and `<root>/src` does not exist.
383    pub fn find_source_dir(root: impl AsRef<Path>) -> PathBuf {
384        utils::find_fave_or_alt_path(root, "src", "contracts")
385    }
386
387    /// Attempts to autodetect the lib directory based on the given root path
388    ///
389    /// Dapptools layout takes precedence over hardhat style.
390    /// This will return:
391    ///   - `<root>/lib` if it exists or `<root>/node_modules` does not exist,
392    ///   - `<root>/node_modules` if it exists and `<root>/lib` does not exist.
393    pub fn find_libs(root: impl AsRef<Path>) -> Vec<PathBuf> {
394        vec![utils::find_fave_or_alt_path(root, "lib", "node_modules")]
395    }
396
397    /// Flattens all file imports into a single string
398    pub fn flatten(&self, target: &Path) -> Result<String> {
399        tracing::trace!("flattening file");
400        let mut input_files = self.input_files();
401
402        // we need to ensure that the target is part of the input set, otherwise it's not
403        // part of the graph if it's not imported by any input file
404        let flatten_target = target.to_path_buf();
405        if !input_files.contains(&flatten_target) {
406            input_files.push(flatten_target);
407        }
408
409        let sources = Source::read_all_files(input_files)?;
410        let graph = Graph::resolve_sources(self, sources)?;
411        self.flatten_node(target, &graph, &mut Default::default(), false, false, false).map(|x| {
412            format!("{}\n", utils::RE_THREE_OR_MORE_NEWLINES.replace_all(&x, "\n\n").trim())
413        })
414    }
415
416    /// Flattens a single node from the dependency graph
417    fn flatten_node(
418        &self,
419        target: &Path,
420        graph: &Graph,
421        imported: &mut HashSet<usize>,
422        strip_version_pragma: bool,
423        strip_experimental_pragma: bool,
424        strip_license: bool,
425    ) -> Result<String> {
426        let target_dir = target.parent().ok_or_else(|| {
427            SolcError::msg(format!("failed to get parent directory for \"{:?}\"", target.display()))
428        })?;
429        let target_index = graph.files().get(target).ok_or_else(|| {
430            SolcError::msg(format!("cannot resolve file at {:?}", target.display()))
431        })?;
432
433        if imported.contains(target_index) {
434            // short circuit nodes that were already imported, if both A.sol and B.sol import C.sol
435            return Ok(String::new())
436        }
437        imported.insert(*target_index);
438
439        let target_node = graph.node(*target_index);
440
441        let mut imports = target_node.imports().clone();
442        imports.sort_by_key(|x| x.loc().start);
443
444        let mut content = target_node.content().to_owned();
445
446        for alias in imports.iter().flat_map(|i| i.data().aliases()) {
447            let (alias, target) = match alias {
448                SolImportAlias::Contract(alias, target) => (alias.clone(), target.clone()),
449                _ => continue,
450            };
451            let name_regex = utils::create_contract_or_lib_name_regex(&alias);
452            let target_len = target.len() as isize;
453            let mut replace_offset = 0;
454            for cap in name_regex.captures_iter(&content.clone()) {
455                if cap.name("ignore").is_some() {
456                    continue
457                }
458                if let Some(name_match) = ["n1", "n2", "n3"].iter().find_map(|name| cap.name(name))
459                {
460                    let name_match_range =
461                        utils::range_by_offset(&name_match.range(), replace_offset);
462                    replace_offset += target_len - (name_match_range.len() as isize);
463                    content.replace_range(name_match_range, &target);
464                }
465            }
466        }
467
468        let mut content = content.as_bytes().to_vec();
469        let mut offset = 0_isize;
470
471        let mut statements = [
472            (target_node.license(), strip_license),
473            (target_node.version(), strip_version_pragma),
474            (target_node.experimental(), strip_experimental_pragma),
475        ]
476        .iter()
477        .filter_map(|(data, condition)| if *condition { data.to_owned().as_ref() } else { None })
478        .collect::<Vec<_>>();
479        statements.sort_by_key(|x| x.loc().start);
480
481        let (mut imports, mut statements) =
482            (imports.iter().peekable(), statements.iter().peekable());
483        while imports.peek().is_some() || statements.peek().is_some() {
484            let (next_import_start, next_statement_start) = (
485                imports.peek().map_or(usize::max_value(), |x| x.loc().start),
486                statements.peek().map_or(usize::max_value(), |x| x.loc().start),
487            );
488            if next_statement_start < next_import_start {
489                let repl_range = statements.next().unwrap().loc_by_offset(offset);
490                offset -= repl_range.len() as isize;
491                content.splice(repl_range, std::iter::empty());
492            } else {
493                let import = imports.next().unwrap();
494                let import_path = self.resolve_import(target_dir, import.data().path())?;
495                let s = self.flatten_node(&import_path, graph, imported, true, true, true)?;
496
497                let import_content = s.as_bytes();
498                let import_content_len = import_content.len() as isize;
499                let import_range = import.loc_by_offset(offset);
500                offset += import_content_len - (import_range.len() as isize);
501                content.splice(import_range, import_content.iter().copied());
502            }
503        }
504
505        let result = String::from_utf8(content).map_err(|err| {
506            SolcError::msg(format!("failed to convert extended bytes to string: {err}"))
507        })?;
508
509        Ok(result)
510    }
511}
512
513impl fmt::Display for ProjectPathsConfig {
514    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
515        writeln!(f, "root: {}", self.root.display())?;
516        writeln!(f, "contracts: {}", self.sources.display())?;
517        writeln!(f, "artifacts: {}", self.artifacts.display())?;
518        writeln!(f, "tests: {}", self.tests.display())?;
519        writeln!(f, "scripts: {}", self.scripts.display())?;
520        writeln!(f, "libs:")?;
521        for lib in &self.libraries {
522            writeln!(f, "    {}", lib.display())?;
523        }
524        writeln!(f, "remappings:")?;
525        for remapping in &self.remappings {
526            writeln!(f, "    {remapping}")?;
527        }
528        Ok(())
529    }
530}
531
532/// This is a subset of [ProjectPathsConfig] that contains all relevant folders in the project
533#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
534pub struct ProjectPaths {
535    pub artifacts: PathBuf,
536    pub build_infos: PathBuf,
537    pub sources: PathBuf,
538    pub tests: PathBuf,
539    pub scripts: PathBuf,
540    pub libraries: BTreeSet<PathBuf>,
541}
542
543impl ProjectPaths {
544    /// Joins the folders' location with `root`
545    pub fn join_all(&mut self, root: impl AsRef<Path>) -> &mut Self {
546        let root = root.as_ref();
547        self.artifacts = root.join(&self.artifacts);
548        self.build_infos = root.join(&self.build_infos);
549        self.sources = root.join(&self.sources);
550        self.tests = root.join(&self.tests);
551        self.scripts = root.join(&self.scripts);
552        let libraries = std::mem::take(&mut self.libraries);
553        self.libraries.extend(libraries.into_iter().map(|p| root.join(p)));
554        self
555    }
556
557    /// Removes `base` from all folders
558    pub fn strip_prefix_all(&mut self, base: impl AsRef<Path>) -> &mut Self {
559        let base = base.as_ref();
560
561        if let Ok(prefix) = self.artifacts.strip_prefix(base) {
562            self.artifacts = prefix.to_path_buf();
563        }
564        if let Ok(prefix) = self.build_infos.strip_prefix(base) {
565            self.build_infos = prefix.to_path_buf();
566        }
567        if let Ok(prefix) = self.sources.strip_prefix(base) {
568            self.sources = prefix.to_path_buf();
569        }
570        if let Ok(prefix) = self.tests.strip_prefix(base) {
571            self.tests = prefix.to_path_buf();
572        }
573        if let Ok(prefix) = self.scripts.strip_prefix(base) {
574            self.scripts = prefix.to_path_buf();
575        }
576        let libraries = std::mem::take(&mut self.libraries);
577        self.libraries.extend(
578            libraries
579                .into_iter()
580                .map(|p| p.strip_prefix(base).map(|p| p.to_path_buf()).unwrap_or(p)),
581        );
582        self
583    }
584}
585
586impl Default for ProjectPaths {
587    fn default() -> Self {
588        Self {
589            artifacts: "out".into(),
590            build_infos: ["out", "build-info"].iter().collect::<PathBuf>(),
591            sources: "src".into(),
592            tests: "test".into(),
593            scripts: "script".into(),
594            libraries: Default::default(),
595        }
596    }
597}
598
599#[derive(Debug, Clone, Eq, PartialEq)]
600pub enum PathStyle {
601    HardHat,
602    Dapptools,
603}
604
605impl PathStyle {
606    /// Convert into a `ProjectPathsConfig` given the root path and based on the styled
607    pub fn paths(&self, root: impl AsRef<Path>) -> Result<ProjectPathsConfig> {
608        let root = root.as_ref();
609        let root = utils::canonicalize(root)?;
610
611        Ok(match self {
612            PathStyle::Dapptools => ProjectPathsConfig::builder()
613                .sources(root.join("src"))
614                .artifacts(root.join("out"))
615                .build_infos(root.join("out").join("build-info"))
616                .lib(root.join("lib"))
617                .remappings(Remapping::find_many(root.join("lib")))
618                .root(root)
619                .build()?,
620            PathStyle::HardHat => ProjectPathsConfig::builder()
621                .sources(root.join("contracts"))
622                .artifacts(root.join("artifacts"))
623                .build_infos(root.join("artifacts").join("build-info"))
624                .lib(root.join("node_modules"))
625                .root(root)
626                .build()?,
627        })
628    }
629}
630
631#[derive(Debug, Clone, Default)]
632pub struct ProjectPathsConfigBuilder {
633    root: Option<PathBuf>,
634    cache: Option<PathBuf>,
635    artifacts: Option<PathBuf>,
636    build_infos: Option<PathBuf>,
637    sources: Option<PathBuf>,
638    tests: Option<PathBuf>,
639    scripts: Option<PathBuf>,
640    libraries: Option<Vec<PathBuf>>,
641    remappings: Option<Vec<Remapping>>,
642}
643
644impl ProjectPathsConfigBuilder {
645    pub fn root(mut self, root: impl Into<PathBuf>) -> Self {
646        self.root = Some(utils::canonicalized(root));
647        self
648    }
649
650    pub fn cache(mut self, cache: impl Into<PathBuf>) -> Self {
651        self.cache = Some(utils::canonicalized(cache));
652        self
653    }
654
655    pub fn artifacts(mut self, artifacts: impl Into<PathBuf>) -> Self {
656        self.artifacts = Some(utils::canonicalized(artifacts));
657        self
658    }
659
660    pub fn build_infos(mut self, build_infos: impl Into<PathBuf>) -> Self {
661        self.build_infos = Some(utils::canonicalized(build_infos));
662        self
663    }
664
665    pub fn sources(mut self, sources: impl Into<PathBuf>) -> Self {
666        self.sources = Some(utils::canonicalized(sources));
667        self
668    }
669
670    pub fn tests(mut self, tests: impl Into<PathBuf>) -> Self {
671        self.tests = Some(utils::canonicalized(tests));
672        self
673    }
674
675    pub fn scripts(mut self, scripts: impl Into<PathBuf>) -> Self {
676        self.scripts = Some(utils::canonicalized(scripts));
677        self
678    }
679
680    /// Specifically disallow additional libraries
681    pub fn no_libs(mut self) -> Self {
682        self.libraries = Some(Vec::new());
683        self
684    }
685
686    pub fn lib(mut self, lib: impl Into<PathBuf>) -> Self {
687        self.libraries.get_or_insert_with(Vec::new).push(utils::canonicalized(lib));
688        self
689    }
690
691    pub fn libs(mut self, libs: impl IntoIterator<Item = impl Into<PathBuf>>) -> Self {
692        let libraries = self.libraries.get_or_insert_with(Vec::new);
693        for lib in libs.into_iter() {
694            libraries.push(utils::canonicalized(lib));
695        }
696        self
697    }
698
699    pub fn remapping(mut self, remapping: Remapping) -> Self {
700        self.remappings.get_or_insert_with(Vec::new).push(remapping);
701        self
702    }
703
704    pub fn remappings(mut self, remappings: impl IntoIterator<Item = Remapping>) -> Self {
705        let our_remappings = self.remappings.get_or_insert_with(Vec::new);
706        for remapping in remappings.into_iter() {
707            our_remappings.push(remapping);
708        }
709        self
710    }
711
712    pub fn build_with_root(self, root: impl Into<PathBuf>) -> ProjectPathsConfig {
713        let root = utils::canonicalized(root);
714
715        let libraries = self.libraries.unwrap_or_else(|| ProjectPathsConfig::find_libs(&root));
716        let artifacts =
717            self.artifacts.unwrap_or_else(|| ProjectPathsConfig::find_artifacts_dir(&root));
718
719        ProjectPathsConfig {
720            cache: self
721                .cache
722                .unwrap_or_else(|| root.join("cache").join(SOLIDITY_FILES_CACHE_FILENAME)),
723            build_infos: self.build_infos.unwrap_or_else(|| artifacts.join("build-info")),
724            artifacts,
725            sources: self.sources.unwrap_or_else(|| ProjectPathsConfig::find_source_dir(&root)),
726            tests: self.tests.unwrap_or_else(|| root.join("test")),
727            scripts: self.scripts.unwrap_or_else(|| root.join("script")),
728            remappings: self
729                .remappings
730                .unwrap_or_else(|| libraries.iter().flat_map(Remapping::find_many).collect()),
731            libraries,
732            root,
733        }
734    }
735
736    pub fn build(self) -> std::result::Result<ProjectPathsConfig, SolcIoError> {
737        let root = self
738            .root
739            .clone()
740            .map(Ok)
741            .unwrap_or_else(std::env::current_dir)
742            .map_err(|err| SolcIoError::new(err, "."))?;
743        Ok(self.build_with_root(root))
744    }
745}
746
747/// The config to use when compiling the contracts
748#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
749pub struct SolcConfig {
750    /// How the file was compiled
751    pub settings: Settings,
752}
753
754impl SolcConfig {
755    /// # Example
756    ///
757    /// Autodetect solc version and default settings
758    ///
759    /// ```rust
760    /// use ethers_solc::SolcConfig;
761    /// let config = SolcConfig::builder().build();
762    /// ```
763    pub fn builder() -> SolcConfigBuilder {
764        SolcConfigBuilder::default()
765    }
766}
767
768impl From<SolcConfig> for Settings {
769    fn from(config: SolcConfig) -> Self {
770        config.settings
771    }
772}
773
774#[derive(Default)]
775pub struct SolcConfigBuilder {
776    settings: Option<Settings>,
777
778    /// additionally selected outputs that should be included in the `Contract` that `solc´ creates
779    output_selection: Vec<ContractOutputSelection>,
780}
781
782impl SolcConfigBuilder {
783    pub fn settings(mut self, settings: Settings) -> Self {
784        self.settings = Some(settings);
785        self
786    }
787
788    /// Adds another `ContractOutputSelection` to the set
789    #[must_use]
790    pub fn additional_output(mut self, output: impl Into<ContractOutputSelection>) -> Self {
791        self.output_selection.push(output.into());
792        self
793    }
794
795    /// Adds multiple `ContractOutputSelection` to the set
796    #[must_use]
797    pub fn additional_outputs<I, S>(mut self, outputs: I) -> Self
798    where
799        I: IntoIterator<Item = S>,
800        S: Into<ContractOutputSelection>,
801    {
802        for out in outputs {
803            self = self.additional_output(out);
804        }
805        self
806    }
807
808    /// Creates the solc config
809    ///
810    /// If no solc version is configured then it will be determined by calling `solc --version`.
811    pub fn build(self) -> SolcConfig {
812        let Self { settings, output_selection } = self;
813        let mut settings = settings.unwrap_or_default();
814        settings.push_all(output_selection);
815        SolcConfig { settings }
816    }
817}
818
819/// Container for all `--include-path` arguments for Solc, see also
820/// [Solc docs](https://docs.soliditylang.org/en/v0.8.9/using-the-compiler.html#base-path-and-import-remapping).
821///
822/// The `--include--path` flag:
823/// > Makes an additional source directory available to the default import callback. Use this option
824/// > if you want to import contracts whose location is not fixed in relation to your main source
825/// > tree, e.g. third-party libraries installed using a package manager. Can be used multiple
826/// > times. Can only be used if base path has a non-empty value.
827///
828/// In contrast to `--allow-paths` [`AllowedLibPaths`], which takes multiple arguments,
829/// `--include-path` only takes a single path argument.
830#[derive(Clone, Debug, Default)]
831pub struct IncludePaths(pub(crate) BTreeSet<PathBuf>);
832
833// === impl IncludePaths ===
834
835impl IncludePaths {
836    /// Returns the [Command](std::process::Command) arguments for this type
837    ///
838    /// For each entry in the set, it will return `--include-path` + `<entry>`
839    pub fn args(&self) -> impl Iterator<Item = String> + '_ {
840        self.paths().flat_map(|path| ["--include-path".to_string(), format!("{}", path.display())])
841    }
842
843    /// Returns all paths that exist
844    pub fn paths(&self) -> impl Iterator<Item = &PathBuf> + '_ {
845        self.0.iter().filter(|path| path.exists())
846    }
847}
848
849impl Deref for IncludePaths {
850    type Target = BTreeSet<PathBuf>;
851
852    fn deref(&self) -> &Self::Target {
853        &self.0
854    }
855}
856
857impl DerefMut for IncludePaths {
858    fn deref_mut(&mut self) -> &mut Self::Target {
859        &mut self.0
860    }
861}
862
863/// Helper struct for serializing `--allow-paths` arguments to Solc
864///
865/// From the [Solc docs](https://docs.soliditylang.org/en/v0.8.9/using-the-compiler.html#base-path-and-import-remapping):
866/// For security reasons the compiler has restrictions on what directories it can access.
867/// Directories of source files specified on the command line and target paths of
868/// remappings are automatically allowed to be accessed by the file reader,
869/// but everything else is rejected by default. Additional paths (and their subdirectories)
870/// can be allowed via the --allow-paths /sample/path,/another/sample/path switch.
871/// Everything inside the path specified via --base-path is always allowed.
872#[derive(Clone, Debug, Default)]
873pub struct AllowedLibPaths(pub(crate) BTreeSet<PathBuf>);
874
875// === impl AllowedLibPaths ===
876
877impl AllowedLibPaths {
878    /// Returns the [Command](std::process::Command) arguments for this type
879    ///
880    /// `--allow-paths` takes a single value: all comma separated paths
881    pub fn args(&self) -> Option<[String; 2]> {
882        let args = self.to_string();
883        if args.is_empty() {
884            return None
885        }
886        Some(["--allow-paths".to_string(), args])
887    }
888
889    /// Returns all paths that exist
890    pub fn paths(&self) -> impl Iterator<Item = &PathBuf> + '_ {
891        self.0.iter().filter(|path| path.exists())
892    }
893}
894
895impl Deref for AllowedLibPaths {
896    type Target = BTreeSet<PathBuf>;
897
898    fn deref(&self) -> &Self::Target {
899        &self.0
900    }
901}
902
903impl DerefMut for AllowedLibPaths {
904    fn deref_mut(&mut self) -> &mut Self::Target {
905        &mut self.0
906    }
907}
908
909impl fmt::Display for AllowedLibPaths {
910    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
911        let lib_paths =
912            self.paths().map(|path| format!("{}", path.display())).collect::<Vec<_>>().join(",");
913        write!(f, "{lib_paths}")
914    }
915}
916
917impl<T: Into<PathBuf>> From<Vec<T>> for AllowedLibPaths {
918    fn from(libs: Vec<T>) -> Self {
919        let libs = libs.into_iter().map(utils::canonicalized).collect();
920        AllowedLibPaths(libs)
921    }
922}
923
924#[cfg(test)]
925mod tests {
926    use super::*;
927
928    #[test]
929    fn can_autodetect_dirs() {
930        let root = utils::tempdir("root").unwrap();
931        let out = root.path().join("out");
932        let artifacts = root.path().join("artifacts");
933        let build_infos = artifacts.join("build-info");
934        let contracts = root.path().join("contracts");
935        let src = root.path().join("src");
936        let lib = root.path().join("lib");
937        let node_modules = root.path().join("node_modules");
938
939        let root = root.path();
940        assert_eq!(ProjectPathsConfig::find_source_dir(root), src,);
941        std::fs::create_dir_all(&contracts).unwrap();
942        assert_eq!(ProjectPathsConfig::find_source_dir(root), contracts,);
943        assert_eq!(
944            ProjectPathsConfig::builder().build_with_root(root).sources,
945            utils::canonicalized(contracts),
946        );
947        std::fs::create_dir_all(&src).unwrap();
948        assert_eq!(ProjectPathsConfig::find_source_dir(root), src,);
949        assert_eq!(
950            ProjectPathsConfig::builder().build_with_root(root).sources,
951            utils::canonicalized(src),
952        );
953
954        assert_eq!(ProjectPathsConfig::find_artifacts_dir(root), out,);
955        std::fs::create_dir_all(&artifacts).unwrap();
956        assert_eq!(ProjectPathsConfig::find_artifacts_dir(root), artifacts,);
957        assert_eq!(
958            ProjectPathsConfig::builder().build_with_root(root).artifacts,
959            utils::canonicalized(artifacts),
960        );
961        std::fs::create_dir_all(&build_infos).unwrap();
962        assert_eq!(
963            ProjectPathsConfig::builder().build_with_root(root).build_infos,
964            utils::canonicalized(build_infos)
965        );
966
967        std::fs::create_dir_all(&out).unwrap();
968        assert_eq!(ProjectPathsConfig::find_artifacts_dir(root), out,);
969        assert_eq!(
970            ProjectPathsConfig::builder().build_with_root(root).artifacts,
971            utils::canonicalized(out),
972        );
973
974        assert_eq!(ProjectPathsConfig::find_libs(root), vec![lib.clone()],);
975        std::fs::create_dir_all(&node_modules).unwrap();
976        assert_eq!(ProjectPathsConfig::find_libs(root), vec![node_modules.clone()],);
977        assert_eq!(
978            ProjectPathsConfig::builder().build_with_root(root).libraries,
979            vec![utils::canonicalized(node_modules)],
980        );
981        std::fs::create_dir_all(&lib).unwrap();
982        assert_eq!(ProjectPathsConfig::find_libs(root), vec![lib.clone()],);
983        assert_eq!(
984            ProjectPathsConfig::builder().build_with_root(root).libraries,
985            vec![utils::canonicalized(lib)],
986        );
987    }
988
989    #[test]
990    fn can_have_sane_build_info_default() {
991        let root = utils::tempdir("root").unwrap();
992        let root = root.path();
993        let artifacts = root.join("forge-artifacts");
994
995        // Set the artifacts directory without setting the
996        // build info directory
997        let project = ProjectPathsConfig::builder().artifacts(&artifacts).build_with_root(root);
998
999        // The artifacts should be set correctly based on the configured value
1000        assert_eq!(project.artifacts, utils::canonicalized(artifacts));
1001
1002        // The build infos should by default in the artifacts directory
1003        assert_eq!(project.build_infos, utils::canonicalized(project.artifacts.join("build-info")));
1004    }
1005
1006    #[test]
1007    #[cfg_attr(windows, ignore = "Windows remappings #2347")]
1008    fn can_find_library_ancestor() {
1009        let mut config = ProjectPathsConfig::builder().lib("lib").build().unwrap();
1010        config.root = "/root/".into();
1011
1012        assert_eq!(config.find_library_ancestor("lib/src/Greeter.sol").unwrap(), Path::new("lib"));
1013
1014        assert_eq!(
1015            config.find_library_ancestor("/root/lib/src/Greeter.sol").unwrap(),
1016            Path::new("lib")
1017        );
1018
1019        config.libraries.push("/root/test/".into());
1020
1021        assert_eq!(
1022            config.find_library_ancestor("test/src/Greeter.sol").unwrap(),
1023            Path::new("/root/test/")
1024        );
1025
1026        assert_eq!(
1027            config.find_library_ancestor("/root/test/src/Greeter.sol").unwrap(),
1028            Path::new("/root/test/")
1029        );
1030    }
1031}