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