Skip to main content

foundry_compilers/
lib.rs

1#![doc = include_str!("../README.md")]
2#![cfg_attr(not(test), warn(unused_crate_dependencies))]
3#![cfg_attr(docsrs, feature(doc_cfg))]
4
5#[macro_use]
6extern crate tracing;
7
8#[cfg(feature = "project-util")]
9#[macro_use]
10extern crate foundry_compilers_core;
11
12mod artifact_output;
13pub use artifact_output::*;
14
15pub mod buildinfo;
16
17pub mod cache;
18
19pub mod flatten;
20
21pub mod resolver;
22pub use resolver::Graph;
23
24pub mod compilers;
25pub use compilers::*;
26
27mod compile;
28pub use compile::{
29    output::{AggregatedCompilerOutput, ProjectCompileOutput},
30    *,
31};
32
33mod config;
34pub use config::{PathStyle, ProjectPaths, ProjectPathsConfig, SolcConfig};
35
36mod filter;
37pub use filter::{FileFilter, SparseOutputFilter, TestFileFilter};
38
39pub mod report;
40
41/// Updates to be applied to the sources.
42///
43/// `source_path -> (start, end, new_value)`
44pub type Updates = HashMap<PathBuf, BTreeSet<(usize, usize, String)>>;
45
46/// Utilities for creating, mocking and testing of (temporary) projects
47#[cfg(feature = "project-util")]
48pub mod project_util;
49
50pub use foundry_compilers_artifacts as artifacts;
51pub use foundry_compilers_core::{error, utils};
52
53use cache::CompilerCache;
54use compile::output::contracts::VersionedContracts;
55use compilers::multi::MultiCompiler;
56use foundry_compilers_artifacts::{
57    output_selection::OutputSelection,
58    solc::{
59        Severity, SourceFile, StandardJsonCompilerInput,
60        sources::{Source, SourceCompilationKind, Sources},
61    },
62};
63use foundry_compilers_core::error::{Result, SolcError, SolcIoError};
64use output::sources::{VersionedSourceFile, VersionedSourceFiles};
65use project::ProjectCompiler;
66use semver::Version;
67use solar::parse::{
68    Parser,
69    interface::{Session, diagnostics::EmittedDiagnostics, source_map::FileName},
70};
71use solc::SolcSettings;
72use std::{
73    collections::{BTreeMap, BTreeSet, HashMap, HashSet},
74    ops::Range,
75    path::{Path, PathBuf},
76    sync::Arc,
77};
78
79/// Represents a project workspace and handles `solc` compiling of all contracts in that workspace.
80#[derive(Clone, derive_more::Debug)]
81pub struct Project<
82    C: Compiler = MultiCompiler,
83    T: ArtifactOutput<CompilerContract = C::CompilerContract> = ConfigurableArtifacts,
84> {
85    pub compiler: C,
86    /// The layout of the project
87    pub paths: ProjectPathsConfig<C::Language>,
88    /// The compiler settings
89    pub settings: C::Settings,
90    /// Additional settings for cases when default compiler settings are not enough to cover all
91    /// possible restrictions.
92    pub additional_settings: BTreeMap<String, C::Settings>,
93    /// Mapping from file path to requirements on settings to compile it.
94    ///
95    /// This file will only be included into compiler inputs with profiles which satisfy the
96    /// restrictions.
97    pub restrictions:
98        BTreeMap<PathBuf, RestrictionsWithVersion<<C::Settings as CompilerSettings>::Restrictions>>,
99    /// Whether caching is enabled
100    pub cached: bool,
101    /// Whether to output build information with each solc call.
102    pub build_info: bool,
103    /// Whether writing artifacts to disk is enabled
104    pub no_artifacts: bool,
105    /// Handles all artifacts related tasks, reading and writing from the artifact dir.
106    pub artifacts: T,
107    /// Errors/Warnings which match these error codes are not going to be logged
108    pub ignored_error_codes: Vec<u64>,
109    /// Errors/Warnings which match these error codes from these path prefixes are not going to be
110    /// logged
111    pub ignored_error_codes_from: Vec<(PathBuf, Vec<u64>)>,
112    /// Errors/Warnings which match these file paths are not going to be logged
113    pub ignored_file_paths: Vec<PathBuf>,
114    /// The minimum severity level that is treated as a compiler error
115    pub compiler_severity_filter: Severity,
116    /// Maximum number of `solc` processes to run simultaneously.
117    solc_jobs: usize,
118    /// Offline mode, if set, network access (download solc) is disallowed
119    pub offline: bool,
120    /// Windows only config value to ensure the all paths use `/` instead of `\\`, same as `solc`
121    ///
122    /// This is a noop on other platforms
123    pub slash_paths: bool,
124    /// Optional sparse output filter used to optimize compilation.
125    #[debug(skip)]
126    pub sparse_output: Option<Box<dyn FileFilter>>,
127}
128
129impl Project {
130    /// Convenience function to call `ProjectBuilder::default()`.
131    ///
132    /// # Examples
133    ///
134    /// Configure with [ConfigurableArtifacts] artifacts output and [MultiCompiler] compiler:
135    /// ```no_run
136    /// use foundry_compilers::Project;
137    ///
138    /// let config = Project::builder().build(Default::default())?;
139    /// # Ok::<(), Box<dyn std::error::Error>>(())
140    /// ```
141    ///
142    /// To configure any a project with any `ArtifactOutput` use either:
143    /// ```no_run
144    /// use foundry_compilers::Project;
145    ///
146    /// let config = Project::builder().build(Default::default())?;
147    /// # Ok::<(), Box<dyn std::error::Error>>(())
148    /// ```
149    ///
150    /// or use the builder directly:
151    /// ```no_run
152    /// use foundry_compilers::{ConfigurableArtifacts, ProjectBuilder, multi::MultiCompiler};
153    ///
154    /// let config = ProjectBuilder::<MultiCompiler>::default().build(Default::default())?;
155    /// # Ok::<(), Box<dyn std::error::Error>>(())
156    /// ```
157    pub fn builder() -> ProjectBuilder {
158        ProjectBuilder::default()
159    }
160}
161
162impl<T: ArtifactOutput<CompilerContract = C::CompilerContract>, C: Compiler> Project<C, T> {
163    /// Returns the handler that takes care of processing all artifacts
164    pub const fn artifacts_handler(&self) -> &T {
165        &self.artifacts
166    }
167
168    pub fn settings_profiles(&self) -> impl Iterator<Item = (&str, &C::Settings)> {
169        std::iter::once(("default", &self.settings))
170            .chain(self.additional_settings.iter().map(|(p, s)| (p.as_str(), s)))
171    }
172}
173
174impl<C: Compiler, T: ArtifactOutput<CompilerContract = C::CompilerContract>> Project<C, T>
175where
176    C::Settings: Into<SolcSettings>,
177{
178    /// Returns standard-json-input to compile the target contract
179    pub fn standard_json_input(&self, target: &Path) -> Result<StandardJsonCompilerInput> {
180        trace!(?target, "Building standard-json-input");
181        let graph = Graph::<C::Parser>::resolve(&self.paths)?;
182        let target_index = graph.files().get(target).ok_or_else(|| {
183            SolcError::msg(format!("cannot resolve file at {:?}", target.display()))
184        })?;
185
186        let mut sources = Vec::new();
187        let mut unique_paths = HashSet::new();
188        let (path, source) = graph.node(*target_index).unpack();
189        unique_paths.insert(path);
190        sources.push((path, source));
191        sources.extend(
192            graph
193                .all_imported_nodes(*target_index)
194                .map(|index| graph.node(index).unpack())
195                .filter(|(p, _)| unique_paths.insert(*p)),
196        );
197
198        let root = self.root();
199        let sources = sources
200            .into_iter()
201            .map(|(path, source)| (rebase_path(root, path), source.clone()))
202            .collect();
203
204        let mut settings = self.settings.clone().into();
205        // strip the path to the project root from all remappings
206        settings.remappings = self
207            .paths
208            .remappings
209            .clone()
210            .into_iter()
211            .map(|r| r.into_relative(self.root()).to_relative_remapping())
212            .collect::<Vec<_>>();
213
214        let input = StandardJsonCompilerInput::new(sources, settings.settings);
215
216        Ok(input)
217    }
218}
219
220impl<T: ArtifactOutput<CompilerContract = C::CompilerContract>, C: Compiler> Project<C, T> {
221    /// Returns the path to the artifacts directory
222    pub fn artifacts_path(&self) -> &Path {
223        &self.paths.artifacts
224    }
225
226    /// Returns the path to the sources directory
227    pub fn sources_path(&self) -> &Path {
228        &self.paths.sources
229    }
230
231    /// Returns the path to the cache file
232    pub fn cache_path(&self) -> &Path {
233        &self.paths.cache
234    }
235
236    /// Returns the path to the `build-info` directory nested in the artifacts dir
237    pub fn build_info_path(&self) -> &Path {
238        &self.paths.build_infos
239    }
240
241    /// Returns the root directory of the project
242    pub fn root(&self) -> &Path {
243        &self.paths.root
244    }
245
246    /// Convenience function to read the cache file.
247    /// See also [CompilerCache::read_joined()]
248    pub fn read_cache_file(&self) -> Result<CompilerCache<C::Settings>> {
249        CompilerCache::read_joined(&self.paths)
250    }
251
252    /// Sets the maximum number of parallel `solc` processes to run simultaneously.
253    ///
254    /// # Panics
255    ///
256    /// if `jobs == 0`
257    pub fn set_solc_jobs(&mut self, jobs: usize) {
258        assert!(jobs > 0);
259        self.solc_jobs = jobs;
260    }
261
262    /// Returns all sources found under the project's configured sources path
263    #[instrument(skip_all, fields(name = "sources"))]
264    pub fn sources(&self) -> Result<Sources> {
265        self.paths.read_sources()
266    }
267
268    /// Emit the cargo [`rerun-if-changed`](https://doc.rust-lang.org/cargo/reference/build-scripts.html#cargorerun-if-changedpath) instruction.
269    ///
270    /// This tells Cargo to re-run the build script if a file inside the project's sources directory
271    /// has changed.
272    ///
273    /// Use this if you compile a project in a `build.rs` file.
274    ///
275    /// # Examples
276    /// ```no_run
277    /// use foundry_compilers::{Project, ProjectPathsConfig};
278    ///
279    /// // Configure the project with all its paths, solc, cache etc.
280    /// // where the root dir is the current Rust project.
281    /// let paths = ProjectPathsConfig::hardhat(env!("CARGO_MANIFEST_DIR").as_ref())?;
282    /// let project = Project::builder().paths(paths).build(Default::default())?;
283    /// let output = project.compile()?;
284    ///
285    /// // Tell Cargo to rerun this build script that if a source file changes.
286    /// project.rerun_if_sources_changed();
287    /// # Ok::<_, Box<dyn std::error::Error>>(())
288    /// ```
289    pub fn rerun_if_sources_changed(&self) {
290        println!("cargo:rerun-if-changed={}", self.paths.sources.display())
291    }
292
293    pub fn compile(&self) -> Result<ProjectCompileOutput<C, T>> {
294        project::ProjectCompiler::new(self)?.compile()
295    }
296
297    /// Convenience function to compile a single solidity file with the project's settings.
298    ///
299    /// # Examples
300    /// ```no_run
301    /// use foundry_compilers::Project;
302    ///
303    /// let project = Project::builder().build(Default::default())?;
304    /// let output = project.compile_file("example/Greeter.sol")?;
305    /// # Ok::<(), Box<dyn std::error::Error>>(())
306    /// ```
307    pub fn compile_file(&self, file: impl Into<PathBuf>) -> Result<ProjectCompileOutput<C, T>> {
308        let file = file.into();
309        let source = Source::read(&file)?;
310        project::ProjectCompiler::with_sources(self, Sources::from([(file, source)]))?.compile()
311    }
312
313    /// Convenience function to compile a series of solidity files with the project's settings.
314    /// Same as [`Self::compile()`] but with the given `files` as input.
315    ///
316    /// # Examples
317    /// ```no_run
318    /// use foundry_compilers::Project;
319    ///
320    /// let project = Project::builder().build(Default::default())?;
321    /// let output = project.compile_files(["examples/Foo.sol", "examples/Bar.sol"])?;
322    /// # Ok::<(), Box<dyn std::error::Error>>(())
323    /// ```
324    pub fn compile_files<P, I>(&self, files: I) -> Result<ProjectCompileOutput<C, T>>
325    where
326        I: IntoIterator<Item = P>,
327        P: Into<PathBuf>,
328    {
329        let sources = Source::read_all(files)?;
330
331        ProjectCompiler::with_sources(self, sources)?.compile()
332    }
333
334    /// Removes the project's artifacts and cache file
335    ///
336    /// If the cache file was the only file in the folder, this also removes the empty folder.
337    ///
338    /// # Examples
339    /// ```
340    /// use foundry_compilers::Project;
341    ///
342    /// let project = Project::builder().build(Default::default())?;
343    /// let _ = project.compile()?;
344    /// assert!(project.artifacts_path().exists());
345    /// assert!(project.cache_path().exists());
346    ///
347    /// project.cleanup();
348    /// assert!(!project.artifacts_path().exists());
349    /// assert!(!project.cache_path().exists());
350    /// # Ok::<(), Box<dyn std::error::Error>>(())
351    /// ```
352    pub fn cleanup(&self) -> std::result::Result<(), SolcIoError> {
353        trace!("clean up project");
354        if self.cache_path().exists() {
355            std::fs::remove_file(self.cache_path())
356                .map_err(|err| SolcIoError::new(err, self.cache_path()))?;
357            if let Some(cache_folder) =
358                self.cache_path().parent().filter(|cache_folder| self.root() != *cache_folder)
359            {
360                // remove the cache folder if the cache file was the only file
361                if cache_folder
362                    .read_dir()
363                    .map_err(|err| SolcIoError::new(err, cache_folder))?
364                    .next()
365                    .is_none()
366                {
367                    std::fs::remove_dir(cache_folder)
368                        .map_err(|err| SolcIoError::new(err, cache_folder))?;
369                }
370            }
371            trace!("removed cache file \"{}\"", self.cache_path().display());
372        }
373
374        // clean the artifacts dir
375        if self.artifacts_path().exists() && self.root() != self.artifacts_path() {
376            std::fs::remove_dir_all(self.artifacts_path())
377                .map_err(|err| SolcIoError::new(err, self.artifacts_path()))?;
378            trace!("removed artifacts dir \"{}\"", self.artifacts_path().display());
379        }
380
381        // also clean the build-info dir, in case it's not nested in the artifacts dir
382        if self.build_info_path().exists() && self.root() != self.build_info_path() {
383            std::fs::remove_dir_all(self.build_info_path())
384                .map_err(|err| SolcIoError::new(err, self.build_info_path()))?;
385            tracing::trace!("removed build-info dir \"{}\"", self.build_info_path().display());
386        }
387
388        Ok(())
389    }
390
391    /// Parses the sources in memory and collects all the contract names mapped to their file paths.
392    fn collect_contract_names(&self) -> Result<HashMap<String, Vec<PathBuf>>>
393    where
394        T: Clone,
395        C: Clone,
396    {
397        let graph = Graph::<C::Parser>::resolve(&self.paths)?;
398        let mut contracts: HashMap<String, Vec<PathBuf>> = HashMap::new();
399        if !graph.is_empty() {
400            for node in &graph.nodes {
401                for contract_name in node.data.contract_names() {
402                    contracts
403                        .entry(contract_name.clone())
404                        .or_default()
405                        .push(node.path().to_path_buf());
406                }
407            }
408        }
409        Ok(contracts)
410    }
411
412    /// Finds the path of the contract with the given name.
413    /// Throws error if multiple or no contracts with the same name are found.
414    pub fn find_contract_path(&self, target_name: &str) -> Result<PathBuf>
415    where
416        T: Clone,
417        C: Clone,
418    {
419        let mut contracts = self.collect_contract_names()?;
420
421        if contracts.get(target_name).is_none_or(|paths| paths.is_empty()) {
422            return Err(SolcError::msg(format!("No contract found with the name `{target_name}`")));
423        }
424        let mut paths = contracts.remove(target_name).unwrap();
425        if paths.len() > 1 {
426            return Err(SolcError::msg(format!(
427                "Multiple contracts found with the name `{target_name}`"
428            )));
429        }
430
431        Ok(paths.remove(0))
432    }
433
434    /// Invokes [CompilerSettings::update_output_selection] on the project's settings and all
435    /// additional settings profiles.
436    pub fn update_output_selection(&mut self, mut f: impl FnMut(&mut OutputSelection)) {
437        self.settings.update_output_selection(&mut f);
438        self.additional_settings.iter_mut().for_each(|(_, s)| {
439            s.update_output_selection(&mut f);
440        });
441    }
442}
443
444pub struct ProjectBuilder<
445    C: Compiler = MultiCompiler,
446    T: ArtifactOutput<CompilerContract = C::CompilerContract> = ConfigurableArtifacts,
447> {
448    /// The layout of the
449    paths: Option<ProjectPathsConfig<C::Language>>,
450    /// How solc invocation should be configured.
451    settings: Option<C::Settings>,
452    additional_settings: BTreeMap<String, C::Settings>,
453    restrictions:
454        BTreeMap<PathBuf, RestrictionsWithVersion<<C::Settings as CompilerSettings>::Restrictions>>,
455    /// Whether caching is enabled, default is true.
456    cached: bool,
457    /// Whether to output build information with each solc call.
458    build_info: bool,
459    /// Whether writing artifacts to disk is enabled, default is true.
460    no_artifacts: bool,
461    /// Use offline mode
462    offline: bool,
463    /// Whether to slash paths of the `ProjectCompilerOutput`
464    slash_paths: bool,
465    /// handles all artifacts related tasks
466    artifacts: T,
467    /// Which error codes to ignore
468    pub ignored_error_codes: Vec<u64>,
469    /// Which error codes to ignore for files matching each path prefix
470    pub ignored_error_codes_from: Vec<(PathBuf, Vec<u64>)>,
471    /// Which file paths to ignore
472    pub ignored_file_paths: Vec<PathBuf>,
473    /// The minimum severity level that is treated as a compiler error
474    compiler_severity_filter: Severity,
475    solc_jobs: Option<usize>,
476    /// Optional sparse output filter used to optimize compilation.
477    sparse_output: Option<Box<dyn FileFilter>>,
478}
479
480impl<C: Compiler, T: ArtifactOutput<CompilerContract = C::CompilerContract>> ProjectBuilder<C, T> {
481    /// Create a new builder with the given artifacts handler
482    pub fn new(artifacts: T) -> Self {
483        Self {
484            paths: None,
485            cached: true,
486            build_info: false,
487            no_artifacts: false,
488            offline: false,
489            slash_paths: true,
490            artifacts,
491            ignored_error_codes: Vec::new(),
492            ignored_error_codes_from: Vec::new(),
493            ignored_file_paths: Vec::new(),
494            compiler_severity_filter: Severity::Error,
495            solc_jobs: None,
496            settings: None,
497            sparse_output: None,
498            additional_settings: BTreeMap::new(),
499            restrictions: BTreeMap::new(),
500        }
501    }
502
503    #[must_use]
504    pub fn paths(mut self, paths: ProjectPathsConfig<C::Language>) -> Self {
505        self.paths = Some(paths);
506        self
507    }
508
509    #[must_use]
510    pub fn settings(mut self, settings: C::Settings) -> Self {
511        self.settings = Some(settings);
512        self
513    }
514
515    #[must_use]
516    pub fn ignore_error_code(mut self, code: u64) -> Self {
517        self.ignored_error_codes.push(code);
518        self
519    }
520
521    #[must_use]
522    pub fn ignore_error_codes(mut self, codes: impl IntoIterator<Item = u64>) -> Self {
523        for code in codes {
524            self = self.ignore_error_code(code);
525        }
526        self
527    }
528
529    #[must_use]
530    pub fn ignore_error_codes_from(
531        mut self,
532        pairs: impl IntoIterator<Item = (PathBuf, Vec<u64>)>,
533    ) -> Self {
534        self.ignored_error_codes_from.extend(pairs);
535        self
536    }
537
538    pub fn ignore_paths(mut self, paths: Vec<PathBuf>) -> Self {
539        self.ignored_file_paths = paths;
540        self
541    }
542
543    #[must_use]
544    pub const fn set_compiler_severity_filter(
545        mut self,
546        compiler_severity_filter: Severity,
547    ) -> Self {
548        self.compiler_severity_filter = compiler_severity_filter;
549        self
550    }
551
552    /// Disables cached builds
553    #[must_use]
554    pub const fn ephemeral(self) -> Self {
555        self.set_cached(false)
556    }
557
558    /// Sets the cache status
559    #[must_use]
560    pub const fn set_cached(mut self, cached: bool) -> Self {
561        self.cached = cached;
562        self
563    }
564
565    /// Sets the build info value
566    #[must_use]
567    pub const fn set_build_info(mut self, build_info: bool) -> Self {
568        self.build_info = build_info;
569        self
570    }
571
572    /// Activates offline mode
573    ///
574    /// Prevents network possible access to download/check solc installs
575    #[must_use]
576    pub const fn offline(self) -> Self {
577        self.set_offline(true)
578    }
579
580    /// Sets the offline status
581    #[must_use]
582    pub const fn set_offline(mut self, offline: bool) -> Self {
583        self.offline = offline;
584        self
585    }
586
587    /// Sets whether to slash all paths on windows
588    ///
589    /// If set to `true` all `\\` separators are replaced with `/`, same as solc
590    #[must_use]
591    pub const fn set_slashed_paths(mut self, slashed_paths: bool) -> Self {
592        self.slash_paths = slashed_paths;
593        self
594    }
595
596    /// Disables writing artifacts to disk
597    #[must_use]
598    pub const fn no_artifacts(self) -> Self {
599        self.set_no_artifacts(true)
600    }
601
602    /// Sets the no artifacts status
603    #[must_use]
604    pub const fn set_no_artifacts(mut self, artifacts: bool) -> Self {
605        self.no_artifacts = artifacts;
606        self
607    }
608
609    /// Sets the maximum number of parallel `solc` processes to run simultaneously.
610    ///
611    /// # Panics
612    ///
613    /// `jobs` must be at least 1
614    #[must_use]
615    pub fn solc_jobs(mut self, jobs: usize) -> Self {
616        assert!(jobs > 0);
617        self.solc_jobs = Some(jobs);
618        self
619    }
620
621    /// Sets the number of parallel `solc` processes to `1`, no parallelization
622    #[must_use]
623    pub fn single_solc_jobs(self) -> Self {
624        self.solc_jobs(1)
625    }
626
627    #[must_use]
628    pub fn sparse_output<F>(mut self, filter: F) -> Self
629    where
630        F: FileFilter + 'static,
631    {
632        self.sparse_output = Some(Box::new(filter));
633        self
634    }
635
636    #[must_use]
637    pub fn additional_settings(mut self, additional: BTreeMap<String, C::Settings>) -> Self {
638        self.additional_settings = additional;
639        self
640    }
641
642    #[must_use]
643    pub fn restrictions(
644        mut self,
645        restrictions: BTreeMap<
646            PathBuf,
647            RestrictionsWithVersion<<C::Settings as CompilerSettings>::Restrictions>,
648        >,
649    ) -> Self {
650        self.restrictions = restrictions;
651        self
652    }
653
654    /// Set arbitrary `ArtifactOutputHandler`
655    pub fn artifacts<A: ArtifactOutput<CompilerContract = C::CompilerContract>>(
656        self,
657        artifacts: A,
658    ) -> ProjectBuilder<C, A> {
659        let Self {
660            paths,
661            cached,
662            no_artifacts,
663            ignored_error_codes,
664            ignored_error_codes_from,
665            compiler_severity_filter,
666            solc_jobs,
667            offline,
668            build_info,
669            slash_paths,
670            ignored_file_paths,
671            settings,
672            sparse_output,
673            additional_settings,
674            restrictions,
675            ..
676        } = self;
677        ProjectBuilder {
678            paths,
679            cached,
680            no_artifacts,
681            additional_settings,
682            restrictions,
683            offline,
684            slash_paths,
685            artifacts,
686            ignored_error_codes,
687            ignored_error_codes_from,
688            ignored_file_paths,
689            compiler_severity_filter,
690            solc_jobs,
691            build_info,
692            settings,
693            sparse_output,
694        }
695    }
696
697    pub fn build(self, compiler: C) -> Result<Project<C, T>> {
698        let Self {
699            paths,
700            cached,
701            no_artifacts,
702            artifacts,
703            ignored_error_codes,
704            ignored_error_codes_from,
705            ignored_file_paths,
706            compiler_severity_filter,
707            solc_jobs,
708            offline,
709            build_info,
710            slash_paths,
711            settings,
712            sparse_output,
713            additional_settings,
714            restrictions,
715        } = self;
716
717        let mut paths = paths.map(Ok).unwrap_or_else(ProjectPathsConfig::current_hardhat)?;
718
719        if slash_paths {
720            // ensures we always use `/` paths
721            paths.slash_paths();
722        }
723
724        Ok(Project {
725            compiler,
726            paths,
727            cached,
728            build_info,
729            no_artifacts,
730            artifacts,
731            ignored_error_codes,
732            ignored_error_codes_from,
733            ignored_file_paths,
734            compiler_severity_filter,
735            solc_jobs: solc_jobs
736                .or_else(|| std::thread::available_parallelism().ok().map(|n| n.get()))
737                .unwrap_or(1),
738            offline,
739            slash_paths,
740            settings: settings.unwrap_or_default(),
741            sparse_output,
742            additional_settings,
743            restrictions,
744        })
745    }
746}
747
748impl<C: Compiler, T: ArtifactOutput<CompilerContract = C::CompilerContract> + Default> Default
749    for ProjectBuilder<C, T>
750{
751    fn default() -> Self {
752        Self::new(T::default())
753    }
754}
755
756impl<T: ArtifactOutput<CompilerContract = C::CompilerContract>, C: Compiler> ArtifactOutput
757    for Project<C, T>
758{
759    type Artifact = T::Artifact;
760    type CompilerContract = C::CompilerContract;
761
762    fn on_output<CP>(
763        &self,
764        contracts: &VersionedContracts<C::CompilerContract>,
765        sources: &VersionedSourceFiles,
766        layout: &ProjectPathsConfig<CP>,
767        ctx: OutputContext<'_>,
768        primary_profiles: &HashMap<PathBuf, &str>,
769    ) -> Result<Artifacts<Self::Artifact>> {
770        self.artifacts_handler().on_output(contracts, sources, layout, ctx, primary_profiles)
771    }
772
773    fn handle_artifacts(
774        &self,
775        contracts: &VersionedContracts<C::CompilerContract>,
776        artifacts: &Artifacts<Self::Artifact>,
777    ) -> Result<()> {
778        self.artifacts_handler().handle_artifacts(contracts, artifacts)
779    }
780
781    fn output_file_name(
782        name: &str,
783        version: &Version,
784        profile: &str,
785        with_version: bool,
786        with_profile: bool,
787    ) -> PathBuf {
788        T::output_file_name(name, version, profile, with_version, with_profile)
789    }
790
791    fn output_file(
792        contract_file: &Path,
793        name: &str,
794        version: &Version,
795        profile: &str,
796        with_version: bool,
797        with_profile: bool,
798    ) -> PathBuf {
799        T::output_file(contract_file, name, version, profile, with_version, with_profile)
800    }
801
802    fn contract_name(file: &Path) -> Option<String> {
803        T::contract_name(file)
804    }
805
806    fn read_cached_artifact(path: &Path) -> Result<Self::Artifact> {
807        T::read_cached_artifact(path)
808    }
809
810    fn read_cached_artifacts<P, I>(files: I) -> Result<BTreeMap<PathBuf, Self::Artifact>>
811    where
812        I: IntoIterator<Item = P>,
813        P: Into<PathBuf>,
814    {
815        T::read_cached_artifacts(files)
816    }
817
818    fn contract_to_artifact(
819        &self,
820        file: &Path,
821        name: &str,
822        contract: C::CompilerContract,
823        source_file: Option<&SourceFile>,
824    ) -> Self::Artifact {
825        self.artifacts_handler().contract_to_artifact(file, name, contract, source_file)
826    }
827
828    fn output_to_artifacts<CP>(
829        &self,
830        contracts: &VersionedContracts<C::CompilerContract>,
831        sources: &VersionedSourceFiles,
832        ctx: OutputContext<'_>,
833        layout: &ProjectPathsConfig<CP>,
834        primary_profiles: &HashMap<PathBuf, &str>,
835    ) -> Artifacts<Self::Artifact> {
836        self.artifacts_handler().output_to_artifacts(
837            contracts,
838            sources,
839            ctx,
840            layout,
841            primary_profiles,
842        )
843    }
844
845    fn standalone_source_file_to_artifact(
846        &self,
847        path: &Path,
848        file: &VersionedSourceFile,
849    ) -> Option<Self::Artifact> {
850        self.artifacts_handler().standalone_source_file_to_artifact(path, file)
851    }
852
853    fn is_dirty(&self, artifact_file: &ArtifactFile<Self::Artifact>) -> Result<bool> {
854        self.artifacts_handler().is_dirty(artifact_file)
855    }
856
857    fn handle_cached_artifacts(&self, artifacts: &Artifacts<Self::Artifact>) -> Result<()> {
858        self.artifacts_handler().handle_cached_artifacts(artifacts)
859    }
860}
861
862// Rebases the given path to the base directory lexically.
863//
864// For instance, given the base `/home/user/project` and the path `/home/user/project/src/A.sol`,
865// this function returns `src/A.sol`.
866//
867// This function transforms a path into a form that is relative to the base directory. The returned
868// path starts either with a normal component (e.g., `src`) or a parent directory component (i.e.,
869// `..`). It also converts the path into a UTF-8 string and replaces all separators with forward
870// slashes (`/`), if they're not.
871//
872// The rebasing process can be conceptualized as follows:
873//
874// 1. Remove the leading components from the path that match those in the base.
875// 2. Prepend `..` components to the path, matching the number of remaining components in the base.
876//
877// # Examples
878//
879// `rebase_path("/home/user/project", "/home/user/project/src/A.sol")` returns `src/A.sol`. The
880// common part, `/home/user/project`, is removed from the path.
881//
882// `rebase_path("/home/user/project", "/home/user/A.sol")` returns `../A.sol`. First, the common
883// part, `/home/user`, is removed, leaving `A.sol`. Next, as `project` remains in the base, `..` is
884// prepended to the path.
885//
886// On Windows, paths like `a\b\c` are converted to `a/b/c`.
887//
888// For more examples, see the test.
889fn rebase_path(base: &Path, path: &Path) -> PathBuf {
890    use path_slash::PathExt;
891
892    let mut base_components = base.components();
893    let mut path_components = path.components();
894
895    let mut new_path = PathBuf::new();
896
897    while let Some(path_component) = path_components.next() {
898        let base_component = base_components.next();
899
900        if Some(path_component) != base_component {
901            if base_component.is_some() {
902                new_path.extend(std::iter::repeat_n(
903                    std::path::Component::ParentDir,
904                    base_components.count() + 1,
905                ));
906            }
907
908            new_path.push(path_component);
909            new_path.extend(path_components);
910
911            break;
912        }
913    }
914
915    new_path.to_slash_lossy().into_owned().into()
916}
917
918/// Utility function to apply a set of updates to provided sources.
919pub fn apply_updates(sources: &mut Sources, updates: Updates) {
920    for (path, source) in sources {
921        if let Some(updates) = updates.get(path) {
922            source.content = Arc::new(replace_source_content(
923                source.content.as_str(),
924                updates.iter().map(|(start, end, update)| ((*start..*end), update.as_str())),
925            ));
926        }
927    }
928}
929
930/// Utility function to change source content ranges with provided updates.
931/// Assumes that the updates are sorted.
932pub fn replace_source_content(
933    source: impl Into<String>,
934    updates: impl IntoIterator<Item = (Range<usize>, impl AsRef<str>)>,
935) -> String {
936    let mut offset = 0;
937    let mut content = source.into();
938    for (range, new_value) in updates {
939        let update_range = utils::range_by_offset(&range, offset);
940        let new_value = new_value.as_ref();
941        content.replace_range(update_range.clone(), new_value);
942        offset += new_value.len() as isize - (update_range.end - update_range.start) as isize;
943    }
944    content
945}
946
947pub(crate) fn parse_one_source<R>(
948    content: &str,
949    path: &Path,
950    f: impl FnOnce(&Session, &solar::parse::ast::SourceUnit<'_>) -> R,
951) -> Result<R, EmittedDiagnostics> {
952    let sess = Session::builder().with_buffer_emitter(Default::default()).build();
953    let res = sess.enter_sequential(|| -> solar::parse::interface::Result<_> {
954        let arena = solar::parse::ast::Arena::new();
955        let filename = FileName::Real(path.to_path_buf());
956        let mut parser = Parser::from_source_code(&sess, &arena, filename, content.to_string())?;
957        let ast = parser.parse_file().map_err(|e| e.emit())?;
958        Ok(f(&sess, &ast))
959    });
960
961    // Return if any diagnostics emitted during content parsing.
962    if let Err(err) = sess.emitted_errors().unwrap() {
963        trace!("failed parsing {path:?}:\n{err}");
964        return Err(err);
965    }
966
967    Ok(res.unwrap())
968}
969
970#[cfg(test)]
971#[cfg(feature = "svm-solc")]
972mod tests {
973    use super::*;
974    use foundry_compilers_artifacts::Remapping;
975    use foundry_compilers_core::utils::{mkdir_or_touch, tempdir};
976
977    #[test]
978    #[cfg_attr(windows, ignore = "<0.7 solc is flaky")]
979    fn test_build_all_versions() {
980        let paths = ProjectPathsConfig::builder()
981            .root("../../test-data/test-contract-versions")
982            .sources("../../test-data/test-contract-versions")
983            .build()
984            .unwrap();
985        let project = Project::builder()
986            .paths(paths)
987            .no_artifacts()
988            .ephemeral()
989            .build(Default::default())
990            .unwrap();
991        let contracts = project.compile().unwrap().succeeded().into_output().contracts;
992        // Contracts A to F
993        assert_eq!(contracts.contracts().count(), 3);
994    }
995
996    #[test]
997    fn test_build_many_libs() {
998        let root = utils::canonicalize("../../test-data/test-contract-libs").unwrap();
999
1000        let paths = ProjectPathsConfig::builder()
1001            .root(&root)
1002            .sources(root.join("src"))
1003            .lib(root.join("lib1"))
1004            .lib(root.join("lib2"))
1005            .remappings(
1006                Remapping::find_many(&root.join("lib1"))
1007                    .into_iter()
1008                    .chain(Remapping::find_many(&root.join("lib2"))),
1009            )
1010            .build()
1011            .unwrap();
1012        let project = Project::builder()
1013            .paths(paths)
1014            .no_artifacts()
1015            .ephemeral()
1016            .no_artifacts()
1017            .build(Default::default())
1018            .unwrap();
1019        let contracts = project.compile().unwrap().succeeded().into_output().contracts;
1020        assert_eq!(contracts.contracts().count(), 3);
1021    }
1022
1023    #[test]
1024    fn test_build_remappings() {
1025        let root = utils::canonicalize("../../test-data/test-contract-remappings").unwrap();
1026        let paths = ProjectPathsConfig::builder()
1027            .root(&root)
1028            .sources(root.join("src"))
1029            .lib(root.join("lib"))
1030            .remappings(Remapping::find_many(&root.join("lib")))
1031            .build()
1032            .unwrap();
1033        let project = Project::builder()
1034            .no_artifacts()
1035            .paths(paths)
1036            .ephemeral()
1037            .build(Default::default())
1038            .unwrap();
1039        let contracts = project.compile().unwrap().succeeded().into_output().contracts;
1040        assert_eq!(contracts.contracts().count(), 2);
1041    }
1042
1043    #[test]
1044    fn can_rebase_path() {
1045        let rebase_path = |a: &str, b: &str| rebase_path(a.as_ref(), b.as_ref());
1046
1047        assert_eq!(rebase_path("a/b", "a/b/c"), PathBuf::from("c"));
1048        assert_eq!(rebase_path("a/b", "a/c"), PathBuf::from("../c"));
1049        assert_eq!(rebase_path("a/b", "c"), PathBuf::from("../../c"));
1050
1051        assert_eq!(
1052            rebase_path("/home/user/project", "/home/user/project/A.sol"),
1053            PathBuf::from("A.sol")
1054        );
1055        assert_eq!(
1056            rebase_path("/home/user/project", "/home/user/project/src/A.sol"),
1057            PathBuf::from("src/A.sol")
1058        );
1059        assert_eq!(
1060            rebase_path("/home/user/project", "/home/user/project/lib/forge-std/src/Test.sol"),
1061            PathBuf::from("lib/forge-std/src/Test.sol")
1062        );
1063        assert_eq!(
1064            rebase_path("/home/user/project", "/home/user/A.sol"),
1065            PathBuf::from("../A.sol")
1066        );
1067        assert_eq!(rebase_path("/home/user/project", "/home/A.sol"), PathBuf::from("../../A.sol"));
1068        assert_eq!(rebase_path("/home/user/project", "/A.sol"), PathBuf::from("../../../A.sol"));
1069        assert_eq!(
1070            rebase_path("/home/user/project", "/tmp/A.sol"),
1071            PathBuf::from("../../../tmp/A.sol")
1072        );
1073
1074        assert_eq!(
1075            rebase_path("/Users/ah/temp/verif", "/Users/ah/temp/remapped/Child.sol"),
1076            PathBuf::from("../remapped/Child.sol")
1077        );
1078        assert_eq!(
1079            rebase_path("/Users/ah/temp/verif", "/Users/ah/temp/verif/../remapped/Parent.sol"),
1080            PathBuf::from("../remapped/Parent.sol")
1081        );
1082    }
1083
1084    #[test]
1085    fn can_resolve_oz_remappings() {
1086        let tmp_dir = tempdir("node_modules").unwrap();
1087        let tmp_dir_node_modules = tmp_dir.path().join("node_modules");
1088        let paths = [
1089            "node_modules/@openzeppelin/contracts/interfaces/IERC1155.sol",
1090            "node_modules/@openzeppelin/contracts/finance/VestingWallet.sol",
1091            "node_modules/@openzeppelin/contracts/proxy/Proxy.sol",
1092            "node_modules/@openzeppelin/contracts/token/ERC20/IERC20.sol",
1093        ];
1094        mkdir_or_touch(tmp_dir.path(), &paths[..]);
1095        let remappings = Remapping::find_many(&tmp_dir_node_modules);
1096        let mut paths = ProjectPathsConfig::<()>::hardhat(tmp_dir.path()).unwrap();
1097        paths.remappings = remappings;
1098
1099        let resolved = paths
1100            .resolve_library_import(
1101                tmp_dir.path(),
1102                Path::new("@openzeppelin/contracts/token/ERC20/IERC20.sol"),
1103            )
1104            .unwrap();
1105        assert!(resolved.exists());
1106
1107        // adjust remappings
1108        paths.remappings[0].name = "@openzeppelin/".to_string();
1109
1110        let resolved = paths
1111            .resolve_library_import(
1112                tmp_dir.path(),
1113                Path::new("@openzeppelin/contracts/token/ERC20/IERC20.sol"),
1114            )
1115            .unwrap();
1116        assert!(resolved.exists());
1117    }
1118
1119    #[test]
1120    fn test_replace_source_content() {
1121        let original_content = r#"
1122library Lib {
1123    function libFn() internal {
1124        // logic to keep
1125    }
1126}
1127contract A {
1128    function a() external {}
1129    function b() public {}
1130    function c() internal {
1131        // logic logic logic
1132    }
1133    function d() private {}
1134    function e() external {
1135        // logic logic logic
1136    }
1137}"#;
1138
1139        let updates = vec![
1140            // Replace function libFn() visibility to external
1141            (36..44, "external"),
1142            // Replace contract A name to contract B
1143            (80..90, "contract B"),
1144            // Remove function c()
1145            (159..222, ""),
1146            // Replace function e() logic
1147            (276..296, "// no logic"),
1148        ];
1149
1150        assert_eq!(
1151            replace_source_content(original_content, updates),
1152            r#"
1153library Lib {
1154    function libFn() external {
1155        // logic to keep
1156    }
1157}
1158contract B {
1159    function a() external {}
1160    function b() public {}
1161    function d() private {}
1162    function e() external {
1163        // no logic
1164    }
1165}"#
1166        );
1167    }
1168}