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::SolParser, 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::<SolParser>::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 true if the given path is a test or script file.
254    pub fn is_test_or_script(&self, path: &Path) -> bool {
255        self.is_test(path) || self.is_script(path)
256    }
257
258    /// Returns true if the given path is a test file.
259    pub fn is_test(&self, path: &Path) -> bool {
260        path_starts_with_rooted(path, &self.tests, &self.root)
261    }
262
263    /// Returns true if the given path is a script file.
264    pub fn is_script(&self, path: &Path) -> bool {
265        path_starts_with_rooted(path, &self.scripts, &self.root)
266    }
267
268    /// Returns true if the given path is a test or script file.
269    pub fn is_source_file(&self, path: &Path) -> bool {
270        !self.is_test_or_script(path)
271    }
272
273    /// Returns a new [ProjectPaths] instance that contains all directories configured for this
274    /// project
275    pub fn paths(&self) -> ProjectPaths {
276        ProjectPaths {
277            artifacts: self.artifacts.clone(),
278            build_infos: self.build_infos.clone(),
279            sources: self.sources.clone(),
280            tests: self.tests.clone(),
281            scripts: self.scripts.clone(),
282            libraries: self.libraries.iter().cloned().collect(),
283        }
284    }
285
286    /// Same as [`paths`][ProjectPathsConfig::paths] but strips the `root` form all paths.
287    ///
288    /// See: [`ProjectPaths::strip_prefix_all`]
289    pub fn paths_relative(&self) -> ProjectPaths {
290        let mut paths = self.paths();
291        paths.strip_prefix_all(&self.root);
292        paths
293    }
294
295    /// Creates all configured dirs and files
296    pub fn create_all(&self) -> std::result::Result<(), SolcIoError> {
297        if let Some(parent) = self.cache.parent() {
298            fs::create_dir_all(parent).map_err(|err| SolcIoError::new(err, parent))?;
299        }
300        fs::create_dir_all(&self.artifacts)
301            .map_err(|err| SolcIoError::new(err, &self.artifacts))?;
302        fs::create_dir_all(&self.sources).map_err(|err| SolcIoError::new(err, &self.sources))?;
303        fs::create_dir_all(&self.tests).map_err(|err| SolcIoError::new(err, &self.tests))?;
304        fs::create_dir_all(&self.scripts).map_err(|err| SolcIoError::new(err, &self.scripts))?;
305        for lib in &self.libraries {
306            fs::create_dir_all(lib).map_err(|err| SolcIoError::new(err, lib))?;
307        }
308        Ok(())
309    }
310
311    /// Converts all `\\` separators in _all_ paths to `/`
312    pub fn slash_paths(&mut self) {
313        #[cfg(windows)]
314        {
315            use path_slash::PathBufExt;
316
317            let slashed = |p: &mut PathBuf| {
318                *p = p.to_slash_lossy().as_ref().into();
319            };
320            slashed(&mut self.root);
321            slashed(&mut self.cache);
322            slashed(&mut self.artifacts);
323            slashed(&mut self.build_infos);
324            slashed(&mut self.sources);
325            slashed(&mut self.tests);
326            slashed(&mut self.scripts);
327
328            self.libraries.iter_mut().for_each(slashed);
329            self.remappings.iter_mut().for_each(Remapping::slash_path);
330
331            self.include_paths = std::mem::take(&mut self.include_paths)
332                .into_iter()
333                .map(|mut p| {
334                    slashed(&mut p);
335                    p
336                })
337                .collect();
338            self.allowed_paths = std::mem::take(&mut self.allowed_paths)
339                .into_iter()
340                .map(|mut p| {
341                    slashed(&mut p);
342                    p
343                })
344                .collect();
345        }
346    }
347
348    /// Returns true if the `file` belongs to a `library`, See [`Self::find_library_ancestor()`]
349    pub fn has_library_ancestor(&self, file: &Path) -> bool {
350        self.find_library_ancestor(file).is_some()
351    }
352
353    /// Returns the library the file belongs to
354    ///
355    /// Returns the first library that is an ancestor of the given `file`.
356    ///
357    /// **Note:** this does not resolve remappings [`Self::resolve_import()`], instead this merely
358    /// checks if a `library` is a parent of `file`
359    ///
360    /// # Examples
361    ///
362    /// ```no_run
363    /// use foundry_compilers::ProjectPathsConfig;
364    /// use std::path::Path;
365    ///
366    /// let config: ProjectPathsConfig = ProjectPathsConfig::builder().lib("lib").build()?;
367    /// assert_eq!(
368    ///     config.find_library_ancestor("lib/src/Greeter.sol".as_ref()),
369    ///     Some(Path::new("lib"))
370    /// );
371    /// Ok::<_, Box<dyn std::error::Error>>(())
372    /// ```
373    pub fn find_library_ancestor(&self, file: &Path) -> Option<&Path> {
374        for lib in &self.libraries {
375            if lib.is_relative()
376                && file.is_absolute()
377                && file.starts_with(&self.root)
378                && file.starts_with(self.root.join(lib))
379                || file.is_relative()
380                    && lib.is_absolute()
381                    && lib.starts_with(&self.root)
382                    && self.root.join(file).starts_with(lib)
383            {
384                return Some(lib);
385            }
386            if file.starts_with(lib) {
387                return Some(lib);
388            }
389        }
390
391        None
392    }
393
394    /// Attempts to resolve an `import` from the given working directory.
395    ///
396    /// The `cwd` path is the parent dir of the file that includes the `import`
397    ///
398    /// This will also populate the `include_paths` with any nested library root paths that should
399    /// be provided to solc via `--include-path` because it uses absolute imports.
400    pub fn resolve_import_and_include_paths(
401        &self,
402        cwd: &Path,
403        import: &Path,
404        include_paths: &mut BTreeSet<PathBuf>,
405    ) -> Result<PathBuf> {
406        let component = import
407            .components()
408            .next()
409            .ok_or_else(|| SolcError::msg(format!("Empty import path {}", import.display())))?;
410
411        if component == Component::CurDir || component == Component::ParentDir {
412            // if the import is relative we assume it's already part of the processed input
413            // file set
414            let resolved = utils::normalize_solidity_import_path(cwd, import).map_err(|err| {
415                SolcError::msg(format!("failed to resolve relative import \"{err:?}\""))
416            })?;
417            // Check if the resolved path matches a remapping and should be redirected
418            Ok(self.apply_remapping_to_path(cwd, &resolved))
419        } else {
420            // resolve library file
421            let resolved = self.resolve_library_import(cwd.as_ref(), import.as_ref());
422
423            if resolved.is_none() {
424                // absolute paths in solidity are a thing for example `import
425                // "src/interfaces/IConfig.sol"` which could either point to `cwd +
426                // src/interfaces/IConfig.sol`, or make use of a remapping (`src/=....`)
427                if let Some(lib) = self.find_library_ancestor(cwd) {
428                    if let Some((include_path, import)) =
429                        utils::resolve_absolute_library(lib, cwd, import)
430                    {
431                        // track the path for this absolute import inside a nested library
432                        include_paths.insert(include_path);
433                        return Ok(import);
434                    }
435                }
436                // also try to resolve absolute imports from the project paths
437                for path in [&self.root, &self.sources, &self.tests, &self.scripts] {
438                    if cwd.starts_with(path) {
439                        if let Ok(import) = utils::normalize_solidity_import_path(path, import) {
440                            return Ok(import);
441                        }
442                    }
443                }
444            }
445
446            resolved.ok_or_else(|| {
447                SolcError::msg(format!(
448                    "failed to resolve library import \"{:?}\"",
449                    import.display()
450                ))
451            })
452        }
453    }
454
455    /// Attempts to resolve an `import` from the given working directory.
456    ///
457    /// The `cwd` path is the parent dir of the file that includes the `import`
458    pub fn resolve_import(&self, cwd: &Path, import: &Path) -> Result<PathBuf> {
459        self.resolve_import_and_include_paths(cwd, import, &mut Default::default())
460    }
461
462    /// Attempts to find the path to the real solidity file that's imported via the given `import`
463    /// path by applying the configured remappings and checking the library dirs
464    ///
465    /// # Examples
466    ///
467    /// Following `@aave` dependency in the `lib` folder `node_modules`
468    ///
469    /// ```text
470    /// <root>/node_modules/@aave
471    /// ├── aave-token
472    /// │   ├── contracts
473    /// │   │   ├── open-zeppelin
474    /// │   │   ├── token
475    /// ├── governance-v2
476    ///     ├── contracts
477    ///         ├── interfaces
478    /// ```
479    ///
480    /// has this remapping: `@aave/=@aave/` (name:path) so contracts can be imported as
481    ///
482    /// ```solidity
483    /// import "@aave/governance-v2/contracts/governance/Executor.sol";
484    /// ```
485    ///
486    /// So that `Executor.sol` can be found by checking each `lib` folder (`node_modules`) with
487    /// applied remappings. Applying remapping works by checking if the import path of an import
488    /// statement starts with the name of a remapping and replacing it with the remapping's `path`.
489    ///
490    /// There are some caveats though, dapptools style remappings usually include the `src` folder
491    /// `ds-test/=lib/ds-test/src/` so that imports look like `import "ds-test/test.sol";` (note the
492    /// missing `src` in the import path).
493    ///
494    /// 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.
495    /// The remapping is detected as `'@openzeppelin/=node_modules/@openzeppelin/contracts/'`, which
496    /// includes the source directory `contracts`, however it's common to see import paths like:
497    ///
498    /// `import "@openzeppelin/contracts/token/ERC20/IERC20.sol";`
499    ///
500    /// instead of
501    ///
502    /// `import "@openzeppelin/token/ERC20/IERC20.sol";`
503    ///
504    /// There is no strict rule behind this, but because
505    /// [`foundry_compilers_artifacts::remappings::Remapping::find_many`] returns `'@
506    /// openzeppelin/=node_modules/@openzeppelin/contracts/'` we should handle the case if the
507    /// remapping path ends with `contracts` and the import path starts with `<remapping
508    /// name>/contracts`. Otherwise we can end up with a resolved path that has a
509    /// duplicate `contracts` segment:
510    /// `@openzeppelin/contracts/contracts/token/ERC20/IERC20.sol` we check for this edge case
511    /// here so that both styles work out of the box.
512    pub fn resolve_library_import(&self, cwd: &Path, import: &Path) -> Option<PathBuf> {
513        // if the import path starts with the name of the remapping then we get the resolved path by
514        // removing the name and adding the remainder to the path of the remapping
515        let cwd = cwd.strip_prefix(&self.root).unwrap_or(cwd);
516        if let Some(path) = self
517            .remappings
518            .iter()
519            .filter(|r| {
520                // only check remappings that are either global or for `cwd`
521                if let Some(ctx) = r.context.as_ref() {
522                    cwd.starts_with(ctx)
523                } else {
524                    true
525                }
526            })
527            .find_map(|r| {
528                import.strip_prefix(&r.name).ok().map(|stripped_import| {
529                    let lib_path =
530                        if stripped_import.as_os_str().is_empty() && r.path.ends_with(".sol") {
531                            r.path.clone().into()
532                        } else {
533                            Path::new(&r.path).join(stripped_import)
534                        };
535
536                    // we handle the edge case where the path of a remapping ends with "contracts"
537                    // (`<name>/=.../contracts`) and the stripped import also starts with
538                    // `contracts`
539                    if let Ok(adjusted_import) = stripped_import.strip_prefix("contracts/") {
540                        if r.path.ends_with("contracts/") && !self.root.join(&lib_path).exists() {
541                            return Path::new(&r.path).join(adjusted_import);
542                        }
543                    }
544                    lib_path
545                })
546            })
547        {
548            Some(self.root.join(path))
549        } else {
550            utils::resolve_library(&self.libraries, import)
551        }
552    }
553
554    /// Applies remappings to a resolved absolute path.
555    ///
556    /// When a file is resolved via relative imports, the resulting path might match a remapping
557    /// pattern. This method checks if the resolved path (relative to the project root) matches
558    /// any remapping and returns the remapped path if so.
559    ///
560    /// This is important for contract verification where the standard JSON input must use
561    /// remapped paths that match what the Solidity compiler expects.
562    ///
563    /// # Arguments
564    /// * `cwd` - The current working directory (parent of the importing file)
565    /// * `resolved_path` - The absolute path that was resolved
566    ///
567    /// # Returns
568    /// The remapped path if a matching remapping exists and the target file exists,
569    /// otherwise returns the original path.
570    pub fn apply_remapping_to_path(&self, cwd: &Path, resolved_path: &Path) -> PathBuf {
571        // Get the path relative to root
572        let relative_path = match resolved_path.strip_prefix(&self.root) {
573            Ok(p) => p,
574            Err(_) => return resolved_path.to_path_buf(),
575        };
576
577        let cwd_relative = cwd.strip_prefix(&self.root).unwrap_or(cwd);
578
579        // Check if any remapping matches this path
580        for r in &self.remappings {
581            // Check context
582            if let Some(ctx) = r.context.as_ref() {
583                if !cwd_relative.starts_with(ctx) {
584                    continue;
585                }
586            }
587
588            // Check if the relative path starts with the remapping name
589            if let Ok(stripped) = relative_path.strip_prefix(&r.name) {
590                let remapped_relative = Path::new(&r.path).join(stripped);
591                let remapped_absolute = self.root.join(&remapped_relative);
592
593                // Only use remapped path if the target file exists
594                if remapped_absolute.exists() {
595                    return remapped_absolute;
596                }
597            }
598        }
599
600        resolved_path.to_path_buf()
601    }
602
603    pub fn with_language_ref<Lang>(&self) -> &ProjectPathsConfig<Lang> {
604        // SAFETY: `Lang` is `PhantomData`.
605        unsafe { std::mem::transmute(self) }
606    }
607
608    pub fn with_language<Lang>(self) -> ProjectPathsConfig<Lang> {
609        // SAFETY: `Lang` is `PhantomData`.
610        unsafe { std::mem::transmute(self) }
611    }
612
613    pub fn apply_lib_remappings(&self, mut libraries: Libraries) -> Libraries {
614        libraries.libs = libraries.libs
615            .into_iter()
616            .map(|(file, target)| {
617                let file = self.resolve_import(&self.root, &file).unwrap_or_else(|err| {
618                    warn!(target: "libs", "Failed to resolve library `{}` for linking: {:?}", file.display(), err);
619                    file
620                });
621                (file, target)
622            })
623            .collect();
624        libraries
625    }
626}
627
628impl<L: Language> ProjectPathsConfig<L> {
629    /// Returns all sources found under the project's configured `sources` path
630    pub fn read_sources(&self) -> Result<Sources> {
631        trace!("reading all sources from \"{}\"", self.sources.display());
632        Ok(Source::read_all_from(&self.sources, L::FILE_EXTENSIONS)?)
633    }
634
635    /// Returns all sources found under the project's configured `test` path
636    pub fn read_tests(&self) -> Result<Sources> {
637        trace!("reading all tests from \"{}\"", self.tests.display());
638        Ok(Source::read_all_from(&self.tests, L::FILE_EXTENSIONS)?)
639    }
640
641    /// Returns all sources found under the project's configured `script` path
642    pub fn read_scripts(&self) -> Result<Sources> {
643        trace!("reading all scripts from \"{}\"", self.scripts.display());
644        Ok(Source::read_all_from(&self.scripts, L::FILE_EXTENSIONS)?)
645    }
646
647    /// Returns true if the there is at least one solidity file in this config.
648    ///
649    /// See also, `Self::input_files()`
650    pub fn has_input_files(&self) -> bool {
651        self.input_files_iter().next().is_some()
652    }
653
654    /// Returns an iterator that yields all solidity file paths for `Self::sources`, `Self::tests`
655    /// and `Self::scripts`
656    pub fn input_files_iter(&self) -> impl Iterator<Item = PathBuf> + '_ {
657        utils::source_files_iter(&self.sources, L::FILE_EXTENSIONS)
658            .chain(utils::source_files_iter(&self.tests, L::FILE_EXTENSIONS))
659            .chain(utils::source_files_iter(&self.scripts, L::FILE_EXTENSIONS))
660    }
661
662    /// Returns the combined set solidity file paths for `Self::sources`, `Self::tests` and
663    /// `Self::scripts`
664    pub fn input_files(&self) -> Vec<PathBuf> {
665        self.input_files_iter().collect()
666    }
667
668    /// Returns the combined set of `Self::read_sources` + `Self::read_tests` + `Self::read_scripts`
669    pub fn read_input_files(&self) -> Result<Sources> {
670        Ok(Source::read_all(self.input_files_iter())?)
671    }
672}
673
674impl fmt::Display for ProjectPathsConfig {
675    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
676        writeln!(f, "root: {}", self.root.display())?;
677        writeln!(f, "contracts: {}", self.sources.display())?;
678        writeln!(f, "artifacts: {}", self.artifacts.display())?;
679        writeln!(f, "tests: {}", self.tests.display())?;
680        writeln!(f, "scripts: {}", self.scripts.display())?;
681        writeln!(f, "libs:")?;
682        for lib in &self.libraries {
683            writeln!(f, "    {}", lib.display())?;
684        }
685        writeln!(f, "remappings:")?;
686        for remapping in &self.remappings {
687            writeln!(f, "    {remapping}")?;
688        }
689        Ok(())
690    }
691}
692
693/// This is a subset of [ProjectPathsConfig] that contains all relevant folders in the project
694#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
695pub struct ProjectPaths {
696    pub artifacts: PathBuf,
697    pub build_infos: PathBuf,
698    pub sources: PathBuf,
699    pub tests: PathBuf,
700    pub scripts: PathBuf,
701    pub libraries: BTreeSet<PathBuf>,
702}
703
704impl ProjectPaths {
705    /// Joins the folders' location with `root`
706    pub fn join_all(&mut self, root: &Path) -> &mut Self {
707        self.artifacts = root.join(&self.artifacts);
708        self.build_infos = root.join(&self.build_infos);
709        self.sources = root.join(&self.sources);
710        self.tests = root.join(&self.tests);
711        self.scripts = root.join(&self.scripts);
712        let libraries = std::mem::take(&mut self.libraries);
713        self.libraries.extend(libraries.into_iter().map(|p| root.join(p)));
714        self
715    }
716
717    /// Removes `base` from all folders
718    pub fn strip_prefix_all(&mut self, base: &Path) -> &mut Self {
719        if let Ok(stripped) = self.artifacts.strip_prefix(base) {
720            self.artifacts = stripped.to_path_buf();
721        }
722        if let Ok(stripped) = self.build_infos.strip_prefix(base) {
723            self.build_infos = stripped.to_path_buf();
724        }
725        if let Ok(stripped) = self.sources.strip_prefix(base) {
726            self.sources = stripped.to_path_buf();
727        }
728        if let Ok(stripped) = self.tests.strip_prefix(base) {
729            self.tests = stripped.to_path_buf();
730        }
731        if let Ok(stripped) = self.scripts.strip_prefix(base) {
732            self.scripts = stripped.to_path_buf();
733        }
734        self.libraries = std::mem::take(&mut self.libraries)
735            .into_iter()
736            .map(|path| strip_prefix_owned(path, base))
737            .collect();
738        self
739    }
740
741    /// Returns true if the given path is a test or script file.
742    pub fn is_test_or_script(&self, path: &Path) -> bool {
743        self.is_test(path) || self.is_script(path)
744    }
745
746    /// Returns true if the given path is a test file.
747    pub fn is_test(&self, path: &Path) -> bool {
748        path.starts_with(&self.tests)
749    }
750
751    /// Returns true if the given path is a script file.
752    pub fn is_script(&self, path: &Path) -> bool {
753        path.starts_with(&self.scripts)
754    }
755
756    /// Returns true if the given path is a test or script file.
757    pub fn is_source_file(&self, path: &Path) -> bool {
758        !self.is_test_or_script(path)
759    }
760}
761
762impl Default for ProjectPaths {
763    fn default() -> Self {
764        Self {
765            artifacts: "out".into(),
766            build_infos: ["out", "build-info"].iter().collect::<PathBuf>(),
767            sources: "src".into(),
768            tests: "test".into(),
769            scripts: "script".into(),
770            libraries: Default::default(),
771        }
772    }
773}
774
775#[derive(Clone, Debug, PartialEq, Eq)]
776pub enum PathStyle {
777    HardHat,
778    Dapptools,
779}
780
781impl PathStyle {
782    /// Convert into a `ProjectPathsConfig` given the root path and based on the styled
783    pub fn paths<C>(&self, root: &Path) -> Result<ProjectPathsConfig<C>> {
784        let root = utils::canonicalize(root)?;
785
786        Ok(match self {
787            Self::Dapptools => ProjectPathsConfig::builder()
788                .sources(root.join("src"))
789                .artifacts(root.join("out"))
790                .build_infos(root.join("out").join("build-info"))
791                .lib(root.join("lib"))
792                .remappings(Remapping::find_many(&root.join("lib")))
793                .root(root)
794                .build()?,
795            Self::HardHat => ProjectPathsConfig::builder()
796                .sources(root.join("contracts"))
797                .artifacts(root.join("artifacts"))
798                .build_infos(root.join("artifacts").join("build-info"))
799                .lib(root.join("node_modules"))
800                .root(root)
801                .build()?,
802        })
803    }
804}
805
806#[derive(Clone, Debug, Default)]
807pub struct ProjectPathsConfigBuilder {
808    root: Option<PathBuf>,
809    cache: Option<PathBuf>,
810    artifacts: Option<PathBuf>,
811    build_infos: Option<PathBuf>,
812    sources: Option<PathBuf>,
813    tests: Option<PathBuf>,
814    scripts: Option<PathBuf>,
815    libraries: Option<Vec<PathBuf>>,
816    remappings: Option<Vec<Remapping>>,
817    include_paths: BTreeSet<PathBuf>,
818    allowed_paths: BTreeSet<PathBuf>,
819}
820
821impl ProjectPathsConfigBuilder {
822    pub fn root(mut self, root: impl Into<PathBuf>) -> Self {
823        self.root = Some(utils::canonicalized(root));
824        self
825    }
826
827    pub fn cache(mut self, cache: impl Into<PathBuf>) -> Self {
828        self.cache = Some(utils::canonicalized(cache));
829        self
830    }
831
832    pub fn artifacts(mut self, artifacts: impl Into<PathBuf>) -> Self {
833        self.artifacts = Some(utils::canonicalized(artifacts));
834        self
835    }
836
837    pub fn build_infos(mut self, build_infos: impl Into<PathBuf>) -> Self {
838        self.build_infos = Some(utils::canonicalized(build_infos));
839        self
840    }
841
842    pub fn sources(mut self, sources: impl Into<PathBuf>) -> Self {
843        self.sources = Some(utils::canonicalized(sources));
844        self
845    }
846
847    pub fn tests(mut self, tests: impl Into<PathBuf>) -> Self {
848        self.tests = Some(utils::canonicalized(tests));
849        self
850    }
851
852    pub fn scripts(mut self, scripts: impl Into<PathBuf>) -> Self {
853        self.scripts = Some(utils::canonicalized(scripts));
854        self
855    }
856
857    /// Specifically disallow additional libraries
858    pub fn no_libs(mut self) -> Self {
859        self.libraries = Some(Vec::new());
860        self
861    }
862
863    pub fn lib(mut self, lib: impl Into<PathBuf>) -> Self {
864        self.libraries.get_or_insert_with(Vec::new).push(utils::canonicalized(lib));
865        self
866    }
867
868    pub fn libs(mut self, libs: impl IntoIterator<Item = impl Into<PathBuf>>) -> Self {
869        let libraries = self.libraries.get_or_insert_with(Vec::new);
870        for lib in libs.into_iter() {
871            libraries.push(utils::canonicalized(lib));
872        }
873        self
874    }
875
876    pub fn remapping(mut self, remapping: Remapping) -> Self {
877        self.remappings.get_or_insert_with(Vec::new).push(remapping);
878        self
879    }
880
881    pub fn remappings(mut self, remappings: impl IntoIterator<Item = Remapping>) -> Self {
882        let our_remappings = self.remappings.get_or_insert_with(Vec::new);
883        for remapping in remappings.into_iter() {
884            our_remappings.push(remapping);
885        }
886        self
887    }
888
889    /// Adds an allowed-path to the solc executable
890    pub fn allowed_path<P: Into<PathBuf>>(mut self, path: P) -> Self {
891        self.allowed_paths.insert(path.into());
892        self
893    }
894
895    /// Adds multiple allowed-path to the solc executable
896    pub fn allowed_paths<I, S>(mut self, args: I) -> Self
897    where
898        I: IntoIterator<Item = S>,
899        S: Into<PathBuf>,
900    {
901        for arg in args {
902            self = self.allowed_path(arg);
903        }
904        self
905    }
906
907    /// Adds an `--include-path` to the solc executable
908    pub fn include_path<P: Into<PathBuf>>(mut self, path: P) -> Self {
909        self.include_paths.insert(path.into());
910        self
911    }
912
913    /// Adds multiple include-path to the solc executable
914    pub fn include_paths<I, S>(mut self, args: I) -> Self
915    where
916        I: IntoIterator<Item = S>,
917        S: Into<PathBuf>,
918    {
919        for arg in args {
920            self = self.include_path(arg);
921        }
922        self
923    }
924
925    pub fn build_with_root<C>(self, root: impl Into<PathBuf>) -> ProjectPathsConfig<C> {
926        let root = utils::canonicalized(root);
927
928        let libraries = self.libraries.unwrap_or_else(|| ProjectPathsConfig::find_libs(&root));
929        let artifacts =
930            self.artifacts.unwrap_or_else(|| ProjectPathsConfig::find_artifacts_dir(&root));
931
932        let mut allowed_paths = self.allowed_paths;
933        // allow every contract under root by default
934        allowed_paths.insert(root.clone());
935
936        ProjectPathsConfig {
937            cache: self
938                .cache
939                .unwrap_or_else(|| root.join("cache").join(SOLIDITY_FILES_CACHE_FILENAME)),
940            build_infos: self.build_infos.unwrap_or_else(|| artifacts.join("build-info")),
941            artifacts,
942            sources: self.sources.unwrap_or_else(|| ProjectPathsConfig::find_source_dir(&root)),
943            tests: self.tests.unwrap_or_else(|| root.join("test")),
944            scripts: self.scripts.unwrap_or_else(|| root.join("script")),
945            remappings: self.remappings.unwrap_or_else(|| {
946                libraries.iter().flat_map(|p| Remapping::find_many(p)).collect()
947            }),
948            libraries,
949            root,
950            include_paths: self.include_paths,
951            allowed_paths,
952            _l: PhantomData,
953        }
954    }
955
956    pub fn build<C>(self) -> std::result::Result<ProjectPathsConfig<C>, SolcIoError> {
957        let root = self
958            .root
959            .clone()
960            .map(Ok)
961            .unwrap_or_else(std::env::current_dir)
962            .map_err(|err| SolcIoError::new(err, "."))?;
963        Ok(self.build_with_root(root))
964    }
965}
966
967/// The config to use when compiling the contracts
968#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
969pub struct SolcConfig {
970    /// How the file was compiled
971    pub settings: Settings,
972}
973
974impl SolcConfig {
975    /// Creates a new [`SolcConfig`] builder.
976    ///
977    /// # Examples
978    ///
979    /// Autodetect solc version and default settings
980    ///
981    /// ```
982    /// use foundry_compilers::SolcConfig;
983    ///
984    /// let config = SolcConfig::builder().build();
985    /// ```
986    pub fn builder() -> SolcConfigBuilder {
987        SolcConfigBuilder::default()
988    }
989}
990
991impl From<SolcConfig> for Settings {
992    fn from(config: SolcConfig) -> Self {
993        config.settings
994    }
995}
996
997#[derive(Default)]
998pub struct SolcConfigBuilder {
999    settings: Option<Settings>,
1000
1001    /// additionally selected outputs that should be included in the `Contract` that solc creates.
1002    output_selection: Vec<ContractOutputSelection>,
1003
1004    /// whether to include the AST in the output
1005    ast: bool,
1006}
1007
1008impl SolcConfigBuilder {
1009    pub fn settings(mut self, settings: Settings) -> Self {
1010        self.settings = Some(settings);
1011        self
1012    }
1013
1014    /// Adds another `ContractOutputSelection` to the set
1015    #[must_use]
1016    pub fn additional_output(mut self, output: impl Into<ContractOutputSelection>) -> Self {
1017        self.output_selection.push(output.into());
1018        self
1019    }
1020
1021    /// Adds multiple `ContractOutputSelection` to the set
1022    #[must_use]
1023    pub fn additional_outputs<I, S>(mut self, outputs: I) -> Self
1024    where
1025        I: IntoIterator<Item = S>,
1026        S: Into<ContractOutputSelection>,
1027    {
1028        for out in outputs {
1029            self = self.additional_output(out);
1030        }
1031        self
1032    }
1033
1034    pub fn ast(mut self, yes: bool) -> Self {
1035        self.ast = yes;
1036        self
1037    }
1038
1039    /// Creates the solc settings
1040    pub fn build(self) -> Settings {
1041        let Self { settings, output_selection, ast } = self;
1042        let mut settings = settings.unwrap_or_default();
1043        settings.push_all(output_selection);
1044        if ast {
1045            settings = settings.with_ast();
1046        }
1047        settings
1048    }
1049}
1050
1051/// Return true if `a` starts with `b` or `b - root`.
1052fn path_starts_with_rooted(a: &Path, b: &Path, root: &Path) -> bool {
1053    if a.starts_with(b) {
1054        return true;
1055    }
1056    if let Ok(b) = b.strip_prefix(root) {
1057        return a.starts_with(b);
1058    }
1059    false
1060}
1061
1062#[cfg(test)]
1063mod tests {
1064    use super::*;
1065
1066    #[test]
1067    fn can_autodetect_dirs() {
1068        let root = utils::tempdir("root").unwrap();
1069        let out = root.path().join("out");
1070        let artifacts = root.path().join("artifacts");
1071        let build_infos = artifacts.join("build-info");
1072        let contracts = root.path().join("contracts");
1073        let src = root.path().join("src");
1074        let lib = root.path().join("lib");
1075        let node_modules = root.path().join("node_modules");
1076
1077        let root = root.path();
1078        assert_eq!(ProjectPathsConfig::find_source_dir(root), src,);
1079        std::fs::create_dir_all(&contracts).unwrap();
1080        assert_eq!(ProjectPathsConfig::find_source_dir(root), contracts,);
1081        assert_eq!(
1082            ProjectPathsConfig::builder().build_with_root::<()>(root).sources,
1083            utils::canonicalized(contracts),
1084        );
1085        std::fs::create_dir_all(&src).unwrap();
1086        assert_eq!(ProjectPathsConfig::find_source_dir(root), src,);
1087        assert_eq!(
1088            ProjectPathsConfig::builder().build_with_root::<()>(root).sources,
1089            utils::canonicalized(src),
1090        );
1091
1092        assert_eq!(ProjectPathsConfig::find_artifacts_dir(root), out,);
1093        std::fs::create_dir_all(&artifacts).unwrap();
1094        assert_eq!(ProjectPathsConfig::find_artifacts_dir(root), artifacts,);
1095        assert_eq!(
1096            ProjectPathsConfig::builder().build_with_root::<()>(root).artifacts,
1097            utils::canonicalized(artifacts),
1098        );
1099        std::fs::create_dir_all(&build_infos).unwrap();
1100        assert_eq!(
1101            ProjectPathsConfig::builder().build_with_root::<()>(root).build_infos,
1102            utils::canonicalized(build_infos)
1103        );
1104
1105        std::fs::create_dir_all(&out).unwrap();
1106        assert_eq!(ProjectPathsConfig::find_artifacts_dir(root), out,);
1107        assert_eq!(
1108            ProjectPathsConfig::builder().build_with_root::<()>(root).artifacts,
1109            utils::canonicalized(out),
1110        );
1111
1112        assert_eq!(ProjectPathsConfig::find_libs(root), vec![lib.clone()],);
1113        std::fs::create_dir_all(&node_modules).unwrap();
1114        assert_eq!(ProjectPathsConfig::find_libs(root), vec![node_modules.clone()],);
1115        assert_eq!(
1116            ProjectPathsConfig::builder().build_with_root::<()>(root).libraries,
1117            vec![utils::canonicalized(node_modules)],
1118        );
1119        std::fs::create_dir_all(&lib).unwrap();
1120        assert_eq!(ProjectPathsConfig::find_libs(root), vec![lib.clone()],);
1121        assert_eq!(
1122            ProjectPathsConfig::builder().build_with_root::<()>(root).libraries,
1123            vec![utils::canonicalized(lib)],
1124        );
1125    }
1126
1127    #[test]
1128    fn can_have_sane_build_info_default() {
1129        let root = utils::tempdir("root").unwrap();
1130        let root = root.path();
1131        let artifacts = root.join("forge-artifacts");
1132
1133        // Set the artifacts directory without setting the
1134        // build info directory
1135        let paths = ProjectPathsConfig::builder().artifacts(&artifacts).build_with_root::<()>(root);
1136
1137        // The artifacts should be set correctly based on the configured value
1138        assert_eq!(paths.artifacts, utils::canonicalized(artifacts));
1139
1140        // The build infos should by default in the artifacts directory
1141        assert_eq!(paths.build_infos, utils::canonicalized(paths.artifacts.join("build-info")));
1142    }
1143
1144    #[test]
1145    #[cfg_attr(windows, ignore = "Windows remappings #2347")]
1146    fn can_find_library_ancestor() {
1147        let mut config = ProjectPathsConfig::builder().lib("lib").build::<()>().unwrap();
1148        config.root = "/root/".into();
1149
1150        assert_eq!(
1151            config.find_library_ancestor("lib/src/Greeter.sol".as_ref()).unwrap(),
1152            Path::new("lib")
1153        );
1154
1155        assert_eq!(
1156            config.find_library_ancestor("/root/lib/src/Greeter.sol".as_ref()).unwrap(),
1157            Path::new("lib")
1158        );
1159
1160        config.libraries.push("/root/test/".into());
1161
1162        assert_eq!(
1163            config.find_library_ancestor("test/src/Greeter.sol".as_ref()).unwrap(),
1164            Path::new("/root/test/")
1165        );
1166
1167        assert_eq!(
1168            config.find_library_ancestor("/root/test/src/Greeter.sol".as_ref()).unwrap(),
1169            Path::new("/root/test/")
1170        );
1171    }
1172
1173    #[test]
1174    fn can_resolve_import() {
1175        let dir = tempfile::tempdir().unwrap();
1176        let config = ProjectPathsConfig::builder().root(dir.path()).build::<()>().unwrap();
1177        config.create_all().unwrap();
1178
1179        fs::write(config.sources.join("A.sol"), r"pragma solidity ^0.8.0; contract A {}").unwrap();
1180
1181        // relative import
1182        assert_eq!(
1183            config
1184                .resolve_import_and_include_paths(
1185                    &config.sources,
1186                    Path::new("./A.sol"),
1187                    &mut Default::default(),
1188                )
1189                .unwrap(),
1190            config.sources.join("A.sol")
1191        );
1192
1193        // direct import
1194        assert_eq!(
1195            config
1196                .resolve_import_and_include_paths(
1197                    &config.sources,
1198                    Path::new("src/A.sol"),
1199                    &mut Default::default(),
1200                )
1201                .unwrap(),
1202            config.sources.join("A.sol")
1203        );
1204    }
1205
1206    #[test]
1207    fn can_resolve_remapped_import() {
1208        let dir = tempfile::tempdir().unwrap();
1209        let mut config = ProjectPathsConfig::builder().root(dir.path()).build::<()>().unwrap();
1210        config.create_all().unwrap();
1211
1212        let dependency = config.root.join("dependency");
1213        fs::create_dir(&dependency).unwrap();
1214        fs::write(dependency.join("A.sol"), r"pragma solidity ^0.8.0; contract A {}").unwrap();
1215
1216        config.remappings.push(Remapping {
1217            context: None,
1218            name: "@dependency/".into(),
1219            path: "dependency/".into(),
1220        });
1221
1222        assert_eq!(
1223            config
1224                .resolve_import_and_include_paths(
1225                    &config.sources,
1226                    Path::new("@dependency/A.sol"),
1227                    &mut Default::default(),
1228                )
1229                .unwrap(),
1230            dependency.join("A.sol")
1231        );
1232    }
1233
1234    #[test]
1235    fn can_resolve_single_file_mapped_import() {
1236        let dir = tempfile::tempdir().unwrap();
1237        let mut config = ProjectPathsConfig::builder().root(dir.path()).build::<()>().unwrap();
1238        config.create_all().unwrap();
1239
1240        fs::write(
1241            config.sources.join("A.sol"),
1242            r#"pragma solidity ^0.8.0; import "@my-lib/B.sol"; contract A is B {}"#,
1243        )
1244        .unwrap();
1245
1246        let dependency = config.root.join("my-lib");
1247        fs::create_dir(&dependency).unwrap();
1248        fs::write(dependency.join("B.sol"), r"pragma solidity ^0.8.0; contract B {}").unwrap();
1249
1250        config.remappings.push(Remapping {
1251            context: None,
1252            name: "@my-lib/B.sol".into(),
1253            path: "my-lib/B.sol".into(),
1254        });
1255
1256        // Test that single file import / remapping resolves to file.
1257        assert!(config
1258            .resolve_import_and_include_paths(
1259                &config.sources,
1260                Path::new("@my-lib/B.sol"),
1261                &mut Default::default(),
1262            )
1263            .unwrap()
1264            .to_str()
1265            .unwrap()
1266            .ends_with("my-lib/B.sol"));
1267    }
1268
1269    #[test]
1270    fn can_apply_remapping_to_relative_import() {
1271        // This test verifies the fix for foundry-rs/foundry#12667
1272        // When a relative import resolves to a path that matches a remapping,
1273        // the remapped path should be used instead.
1274        let dir = tempfile::tempdir().unwrap();
1275        let mut config = ProjectPathsConfig::builder().root(dir.path()).build::<()>().unwrap();
1276        config.create_all().unwrap();
1277
1278        // Create lib/external/utils/LibMem/LibMem.sol (the original external file)
1279        let lib_dir = config.root.join("lib/external/utils/LibMem");
1280        fs::create_dir_all(&lib_dir).unwrap();
1281        fs::write(
1282            lib_dir.join("LibMem.sol"),
1283            r"pragma solidity ^0.8.0; library LibMem { bool constant EXTERNAL = true; }",
1284        )
1285        .unwrap();
1286
1287        // Create src/utils/LibMem.sol (the local override)
1288        let local_dir = config.root.join("src/utils");
1289        fs::create_dir_all(&local_dir).unwrap();
1290        fs::write(
1291            local_dir.join("LibMem.sol"),
1292            r"pragma solidity ^0.8.0; library LibMem { bool constant LOCAL = true; }",
1293        )
1294        .unwrap();
1295
1296        // Create a file that uses relative import
1297        let importing_file = config.root.join("lib/external/utils/BytesUtils.sol");
1298        fs::write(&importing_file, r#"pragma solidity ^0.8.0; import "./LibMem/LibMem.sol";"#)
1299            .unwrap();
1300
1301        // Add remapping: lib/external/utils/LibMem/=src/utils/
1302        config.remappings.push(Remapping {
1303            context: None,
1304            name: "lib/external/utils/LibMem/".into(),
1305            path: "src/utils/".into(),
1306        });
1307
1308        // Resolve the relative import from BytesUtils.sol
1309        let cwd = importing_file.parent().unwrap();
1310        let resolved = config
1311            .resolve_import_and_include_paths(
1312                cwd,
1313                Path::new("./LibMem/LibMem.sol"),
1314                &mut Default::default(),
1315            )
1316            .unwrap();
1317
1318        // The resolved path should be the local override, not the external file
1319        assert_eq!(resolved, local_dir.join("LibMem.sol"));
1320    }
1321}