foundry_compilers/
config.rs

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