Skip to main content

foundry_compilers/artifact_output/
mod.rs

1//! Output artifact handling
2
3use alloy_json_abi::JsonAbi;
4use alloy_primitives::Bytes;
5use foundry_compilers_artifacts::{
6    BytecodeObject, CompactBytecode, CompactContract, CompactContractBytecode,
7    CompactContractBytecodeCow, CompactDeployedBytecode, Contract, FileToContractsMap, SourceFile,
8    hh::HardhatArtifact,
9    sourcemap::{SourceMap, SyntaxError},
10};
11use foundry_compilers_core::{
12    error::{Result, SolcError, SolcIoError},
13    utils::{self, strip_prefix_owned},
14};
15use path_slash::PathBufExt;
16use semver::Version;
17use serde::{Deserialize, Serialize, de::DeserializeOwned};
18use std::{
19    borrow::Cow,
20    collections::{HashMap, HashSet, btree_map::BTreeMap},
21    ffi::OsString,
22    fmt, fs,
23    hash::Hash,
24    ops::Deref,
25    path::{Path, PathBuf},
26};
27
28mod configurable;
29pub use configurable::*;
30
31mod hh;
32pub use hh::*;
33
34use crate::{
35    CompilerContract, ProjectPathsConfig,
36    cache::{CachedArtifacts, CompilerCache},
37    output::{
38        contracts::VersionedContracts,
39        sources::{VersionedSourceFile, VersionedSourceFiles},
40    },
41};
42
43/// Represents unique artifact metadata for identifying artifacts on output
44#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
45pub struct ArtifactId {
46    /// `artifact` cache path
47    pub path: PathBuf,
48    pub name: String,
49    /// Original source file path
50    pub source: PathBuf,
51    /// `solc` version that produced this artifact
52    pub version: Version,
53    /// `solc` build id
54    pub build_id: String,
55    pub profile: String,
56}
57
58impl ArtifactId {
59    /// Converts any `\\` separators in the `path` to `/`
60    #[allow(clippy::missing_const_for_fn)]
61    pub fn slash_paths(&mut self) {
62        #[cfg(windows)]
63        {
64            self.path = self.path.to_slash_lossy().as_ref().into();
65            self.source = self.source.to_slash_lossy().as_ref().into();
66        }
67    }
68
69    /// Convenience function for [`Self::slash_paths()`]
70    #[allow(clippy::missing_const_for_fn)]
71    pub fn with_slashed_paths(mut self) -> Self {
72        self.slash_paths();
73        self
74    }
75
76    /// Removes `base` from the source's path.
77    pub fn strip_file_prefixes(&mut self, base: &Path) {
78        if let Ok(stripped) = self.source.strip_prefix(base) {
79            self.source = stripped.to_path_buf();
80        }
81    }
82
83    /// Convenience function for [`Self::strip_file_prefixes()`]
84    pub fn with_stripped_file_prefixes(mut self, base: &Path) -> Self {
85        self.strip_file_prefixes(base);
86        self
87    }
88
89    /// Returns a `<filename>:<name>` slug that identifies an artifact
90    ///
91    /// Note: This identifier is not necessarily unique. If two contracts have the same name, they
92    /// will share the same slug. For a unique identifier see [ArtifactId::identifier].
93    pub fn slug(&self) -> String {
94        format!("{}.json:{}", self.path.file_stem().unwrap().to_string_lossy(), self.name)
95    }
96
97    /// Returns a `<source path>:<name>` slug that uniquely identifies an artifact
98    pub fn identifier(&self) -> String {
99        format!("{}:{}", self.source.display(), self.name)
100    }
101
102    /// Returns a `<filename><version>:<name>` slug that identifies an artifact
103    pub fn slug_versioned(&self) -> String {
104        format!(
105            "{}.{}.{}.{}.json:{}",
106            self.path.file_stem().unwrap().to_string_lossy(),
107            self.version.major,
108            self.version.minor,
109            self.version.patch,
110            self.name
111        )
112    }
113}
114
115/// Represents an artifact file representing a [`crate::compilers::CompilerContract`]
116#[derive(Clone, Debug, PartialEq, Eq)]
117pub struct ArtifactFile<T> {
118    /// The Artifact that was written
119    pub artifact: T,
120    /// path to the file where the `artifact` was written to
121    pub file: PathBuf,
122    /// `solc` version that produced this artifact
123    pub version: Version,
124    pub build_id: String,
125    pub profile: String,
126}
127
128impl<T: Serialize> ArtifactFile<T> {
129    /// Writes the given contract to the `out` path creating all parent directories
130    pub fn write(&self) -> Result<()> {
131        trace!("writing artifact file {:?} {}", self.file, self.version);
132        utils::create_parent_dir_all(&self.file)?;
133        utils::write_json_file(&self.artifact, &self.file, 64 * 1024)
134    }
135}
136
137impl<T> ArtifactFile<T> {
138    /// Sets the file to `root` adjoined to `self.file`.
139    pub fn join(&mut self, root: &Path) {
140        self.file = root.join(&self.file);
141    }
142
143    /// Removes `base` from the artifact's path
144    pub fn strip_prefix(&mut self, base: &Path) {
145        if let Ok(stripped) = self.file.strip_prefix(base) {
146            self.file = stripped.to_path_buf();
147        }
148    }
149}
150
151/// local helper type alias `file name -> (contract name  -> Vec<..>)`
152pub(crate) type ArtifactsMap<T> = FileToContractsMap<Vec<ArtifactFile<T>>>;
153
154/// Represents a set of Artifacts
155#[derive(Clone, Debug, PartialEq, Eq)]
156pub struct Artifacts<T>(pub ArtifactsMap<T>);
157
158impl<T> From<ArtifactsMap<T>> for Artifacts<T> {
159    fn from(m: ArtifactsMap<T>) -> Self {
160        Self(m)
161    }
162}
163
164impl<'a, T> IntoIterator for &'a Artifacts<T> {
165    type Item = (&'a PathBuf, &'a BTreeMap<String, Vec<ArtifactFile<T>>>);
166    type IntoIter =
167        std::collections::btree_map::Iter<'a, PathBuf, BTreeMap<String, Vec<ArtifactFile<T>>>>;
168
169    fn into_iter(self) -> Self::IntoIter {
170        self.0.iter()
171    }
172}
173
174impl<T> IntoIterator for Artifacts<T> {
175    type Item = (PathBuf, BTreeMap<String, Vec<ArtifactFile<T>>>);
176    type IntoIter =
177        std::collections::btree_map::IntoIter<PathBuf, BTreeMap<String, Vec<ArtifactFile<T>>>>;
178
179    fn into_iter(self) -> Self::IntoIter {
180        self.0.into_iter()
181    }
182}
183
184impl<T> Default for Artifacts<T> {
185    fn default() -> Self {
186        Self(Default::default())
187    }
188}
189
190impl<T> AsRef<ArtifactsMap<T>> for Artifacts<T> {
191    fn as_ref(&self) -> &ArtifactsMap<T> {
192        &self.0
193    }
194}
195
196impl<T> AsMut<ArtifactsMap<T>> for Artifacts<T> {
197    fn as_mut(&mut self) -> &mut ArtifactsMap<T> {
198        &mut self.0
199    }
200}
201
202impl<T> Deref for Artifacts<T> {
203    type Target = ArtifactsMap<T>;
204
205    fn deref(&self) -> &Self::Target {
206        &self.0
207    }
208}
209
210impl<T: Serialize> Artifacts<T> {
211    /// Writes all artifacts into the given `artifacts_root` folder
212    pub fn write_all(&self) -> Result<()> {
213        for artifact in self.artifact_files() {
214            artifact.write()?;
215        }
216        Ok(())
217    }
218}
219
220impl<T> Artifacts<T> {
221    /// Converts all `\\` separators in _all_ paths to `/`
222    #[allow(clippy::missing_const_for_fn)]
223    pub fn slash_paths(&mut self) {
224        #[cfg(windows)]
225        {
226            self.0 = std::mem::take(&mut self.0)
227                .into_iter()
228                .map(|(path, files)| (PathBuf::from(path.to_slash_lossy().as_ref()), files))
229                .collect()
230        }
231    }
232
233    pub fn into_inner(self) -> ArtifactsMap<T> {
234        self.0
235    }
236
237    /// Sets the artifact files location to `root` adjoined to `self.file`.
238    pub fn join_all(&mut self, root: &Path) -> &mut Self {
239        self.artifact_files_mut().for_each(|artifact| artifact.join(root));
240        self
241    }
242
243    /// Removes `base` from all artifacts
244    pub fn strip_prefix_all(&mut self, base: &Path) -> &mut Self {
245        self.artifact_files_mut().for_each(|artifact| artifact.strip_prefix(base));
246        self
247    }
248
249    /// Returns all `ArtifactFile`s for the contract with the matching name
250    fn get_contract_artifact_files(&self, contract_name: &str) -> Option<&Vec<ArtifactFile<T>>> {
251        self.0.values().find_map(|all| all.get(contract_name))
252    }
253
254    /// Returns the `Artifact` with matching file, contract name and version
255    pub fn find_artifact(
256        &self,
257        file: &Path,
258        contract_name: &str,
259        version: &Version,
260    ) -> Option<&ArtifactFile<T>> {
261        self.0
262            .get(file)
263            .and_then(|contracts| contracts.get(contract_name))
264            .and_then(|artifacts| artifacts.iter().find(|artifact| artifact.version == *version))
265    }
266
267    /// Returns the `Artifact` with matching file, contract name, version and profile
268    pub fn find_artifact_with_profile(
269        &self,
270        file: &Path,
271        contract_name: &str,
272        version: &Version,
273        profile: &str,
274    ) -> Option<&ArtifactFile<T>> {
275        self.0.get(file).and_then(|contracts| contracts.get(contract_name)).and_then(|artifacts| {
276            artifacts
277                .iter()
278                .find(|artifact| artifact.version == *version && artifact.profile == profile)
279        })
280    }
281
282    /// Returns true if this type contains an artifact with the given path for the given contract
283    pub fn has_contract_artifact(&self, contract_name: &str, artifact_path: &Path) -> bool {
284        self.get_contract_artifact_files(contract_name)
285            .map(|artifacts| artifacts.iter().any(|artifact| artifact.file == artifact_path))
286            .unwrap_or_default()
287    }
288
289    /// Returns true if this type contains an artifact with the given path
290    pub fn has_artifact(&self, artifact_path: &Path) -> bool {
291        self.artifact_files().any(|artifact| artifact.file == artifact_path)
292    }
293
294    /// Iterate over all artifact files
295    pub fn artifact_files(&self) -> impl Iterator<Item = &ArtifactFile<T>> {
296        self.0.values().flat_map(BTreeMap::values).flatten()
297    }
298
299    /// Iterate over all artifact files
300    pub fn artifact_files_mut(&mut self) -> impl Iterator<Item = &mut ArtifactFile<T>> {
301        self.0.values_mut().flat_map(BTreeMap::values_mut).flatten()
302    }
303
304    /// Returns an iterator over _all_ artifacts and `<file name:contract name>`.
305    ///
306    /// Borrowed version of [`Self::into_artifacts`].
307    pub fn artifacts<O: ArtifactOutput<Artifact = T>>(
308        &self,
309    ) -> impl Iterator<Item = (ArtifactId, &T)> + '_ {
310        self.0.iter().flat_map(|(source, contract_artifacts)| {
311            contract_artifacts.values().flat_map(move |artifacts| {
312                artifacts.iter().filter_map(move |artifact| {
313                    O::contract_name(&artifact.file).map(|name| {
314                        (
315                            ArtifactId {
316                                path: PathBuf::from(&artifact.file),
317                                name,
318                                source: source.clone(),
319                                version: artifact.version.clone(),
320                                build_id: artifact.build_id.clone(),
321                                profile: artifact.profile.clone(),
322                            }
323                            .with_slashed_paths(),
324                            &artifact.artifact,
325                        )
326                    })
327                })
328            })
329        })
330    }
331
332    /// Returns an iterator over _all_ artifacts and `<file name:contract name>`
333    pub fn into_artifacts<O: ArtifactOutput<Artifact = T>>(
334        self,
335    ) -> impl Iterator<Item = (ArtifactId, T)> {
336        self.0.into_iter().flat_map(|(source, contract_artifacts)| {
337            contract_artifacts.into_values().flat_map(move |artifacts| {
338                let source = source.clone();
339                artifacts.into_iter().filter_map(move |artifact| {
340                    O::contract_name(&artifact.file).map(|name| {
341                        (
342                            ArtifactId {
343                                path: PathBuf::from(&artifact.file),
344                                name,
345                                source: source.clone(),
346                                version: artifact.version,
347                                build_id: artifact.build_id.clone(),
348                                profile: artifact.profile.clone(),
349                            }
350                            .with_slashed_paths(),
351                            artifact.artifact,
352                        )
353                    })
354                })
355            })
356        })
357    }
358
359    /// Returns an iterator that yields the tuple `(file, contract name, artifact)`
360    ///
361    /// **NOTE** this returns the path as is
362    ///
363    /// Borrowed version of [`Self::into_artifacts_with_files`].
364    pub fn artifacts_with_files(&self) -> impl Iterator<Item = (&PathBuf, &String, &T)> + '_ {
365        self.0.iter().flat_map(|(f, contract_artifacts)| {
366            contract_artifacts.iter().flat_map(move |(name, artifacts)| {
367                artifacts.iter().map(move |artifact| (f, name, &artifact.artifact))
368            })
369        })
370    }
371
372    /// Returns an iterator that yields the tuple `(file, contract name, artifact)`
373    ///
374    /// **NOTE** this returns the path as is
375    pub fn into_artifacts_with_files(self) -> impl Iterator<Item = (PathBuf, String, T)> {
376        self.0.into_iter().flat_map(|(f, contract_artifacts)| {
377            contract_artifacts.into_iter().flat_map(move |(name, artifacts)| {
378                let contract_name = name;
379                let file = f.clone();
380                artifacts
381                    .into_iter()
382                    .map(move |artifact| (file.clone(), contract_name.clone(), artifact.artifact))
383            })
384        })
385    }
386
387    /// Strips the given prefix from all artifact file paths to make them relative to the given
388    /// `root` argument
389    pub fn into_stripped_file_prefixes(self, base: &Path) -> Self {
390        let artifacts =
391            self.0.into_iter().map(|(path, c)| (strip_prefix_owned(path, base), c)).collect();
392        Self(artifacts)
393    }
394
395    /// Finds the first artifact `T` with a matching contract name
396    pub fn find_first(&self, contract_name: &str) -> Option<&T> {
397        self.0.iter().find_map(|(_file, contracts)| {
398            contracts.get(contract_name).and_then(|c| c.first().map(|a| &a.artifact))
399        })
400    }
401
402    ///  Finds the artifact with a matching path and name
403    pub fn find(&self, contract_path: &Path, contract_name: &str) -> Option<&T> {
404        self.0.iter().filter(|(path, _)| path.as_path() == contract_path).find_map(
405            |(_file, contracts)| {
406                contracts.get(contract_name).and_then(|c| c.first().map(|a| &a.artifact))
407            },
408        )
409    }
410
411    /// Removes the artifact with matching file and name
412    pub fn remove(&mut self, contract_path: &Path, contract_name: &str) -> Option<T> {
413        self.0.iter_mut().filter(|(path, _)| path.as_path() == contract_path).find_map(
414            |(_file, contracts)| {
415                let mut artifact = None;
416                if let Some((c, mut artifacts)) = contracts.remove_entry(contract_name) {
417                    if !artifacts.is_empty() {
418                        artifact = Some(artifacts.remove(0).artifact);
419                    }
420                    if !artifacts.is_empty() {
421                        contracts.insert(c, artifacts);
422                    }
423                }
424                artifact
425            },
426        )
427    }
428
429    /// Removes the first artifact `T` with a matching contract name
430    ///
431    /// *Note:* if there are multiple artifacts (contract compiled with different solc) then this
432    /// returns the first artifact in that set
433    pub fn remove_first(&mut self, contract_name: &str) -> Option<T> {
434        self.0.iter_mut().find_map(|(_file, contracts)| {
435            let mut artifact = None;
436            if let Some((c, mut artifacts)) = contracts.remove_entry(contract_name) {
437                if !artifacts.is_empty() {
438                    artifact = Some(artifacts.remove(0).artifact);
439                }
440                if !artifacts.is_empty() {
441                    contracts.insert(c, artifacts);
442                }
443            }
444            artifact
445        })
446    }
447}
448
449/// A trait representation for a [`crate::compilers::CompilerContract`] artifact
450pub trait Artifact {
451    /// Returns the artifact's [`JsonAbi`] and bytecode.
452    fn into_inner(self) -> (Option<JsonAbi>, Option<Bytes>);
453
454    /// Turns the artifact into a container type for abi, compact bytecode and deployed bytecode
455    fn into_compact_contract(self) -> CompactContract;
456
457    /// Turns the artifact into a container type for abi, full bytecode and deployed bytecode
458    fn into_contract_bytecode(self) -> CompactContractBytecode;
459
460    /// Returns the contents of this type as a single tuple of abi, bytecode and deployed bytecode
461    fn into_parts(self) -> (Option<JsonAbi>, Option<Bytes>, Option<Bytes>);
462
463    /// Consumes the type and returns the [JsonAbi]
464    fn into_abi(self) -> Option<JsonAbi>
465    where
466        Self: Sized,
467    {
468        self.into_parts().0
469    }
470
471    /// Consumes the type and returns the `bytecode`
472    fn into_bytecode_bytes(self) -> Option<Bytes>
473    where
474        Self: Sized,
475    {
476        self.into_parts().1
477    }
478    /// Consumes the type and returns the `deployed bytecode`
479    fn into_deployed_bytecode_bytes(self) -> Option<Bytes>
480    where
481        Self: Sized,
482    {
483        self.into_parts().2
484    }
485
486    /// Same as [`Self::into_parts()`] but returns `Err` if an element is `None`
487    fn try_into_parts(self) -> Result<(JsonAbi, Bytes, Bytes)>
488    where
489        Self: Sized,
490    {
491        let (abi, bytecode, deployed_bytecode) = self.into_parts();
492
493        Ok((
494            abi.ok_or_else(|| SolcError::msg("abi missing"))?,
495            bytecode.ok_or_else(|| SolcError::msg("bytecode missing"))?,
496            deployed_bytecode.ok_or_else(|| SolcError::msg("deployed bytecode missing"))?,
497        ))
498    }
499
500    /// Returns the reference of container type for abi, compact bytecode and deployed bytecode if
501    /// available
502    fn get_contract_bytecode(&self) -> CompactContractBytecodeCow<'_>;
503
504    /// Returns the reference to the `bytecode`
505    fn get_bytecode(&self) -> Option<Cow<'_, CompactBytecode>> {
506        self.get_contract_bytecode().bytecode
507    }
508
509    /// Returns the reference to the `bytecode` object
510    fn get_bytecode_object(&self) -> Option<Cow<'_, BytecodeObject>> {
511        let val = match self.get_bytecode()? {
512            Cow::Borrowed(b) => Cow::Borrowed(&b.object),
513            Cow::Owned(b) => Cow::Owned(b.object),
514        };
515        Some(val)
516    }
517
518    /// Returns the bytes of the `bytecode` object
519    fn get_bytecode_bytes(&self) -> Option<Cow<'_, Bytes>> {
520        let val = match self.get_bytecode_object()? {
521            Cow::Borrowed(b) => Cow::Borrowed(b.as_bytes()?),
522            Cow::Owned(b) => Cow::Owned(b.into_bytes()?),
523        };
524        Some(val)
525    }
526
527    /// Returns the reference to the `deployedBytecode`
528    fn get_deployed_bytecode(&self) -> Option<Cow<'_, CompactDeployedBytecode>> {
529        self.get_contract_bytecode().deployed_bytecode
530    }
531
532    /// Returns the reference to the `bytecode` object
533    fn get_deployed_bytecode_object(&self) -> Option<Cow<'_, BytecodeObject>> {
534        let val = match self.get_deployed_bytecode()? {
535            Cow::Borrowed(b) => Cow::Borrowed(&b.bytecode.as_ref()?.object),
536            Cow::Owned(b) => Cow::Owned(b.bytecode?.object),
537        };
538        Some(val)
539    }
540
541    /// Returns the bytes of the `deployed bytecode` object
542    fn get_deployed_bytecode_bytes(&self) -> Option<Cow<'_, Bytes>> {
543        let val = match self.get_deployed_bytecode_object()? {
544            Cow::Borrowed(b) => Cow::Borrowed(b.as_bytes()?),
545            Cow::Owned(b) => Cow::Owned(b.into_bytes()?),
546        };
547        Some(val)
548    }
549
550    /// Returns the reference to the [JsonAbi] if available
551    fn get_abi(&self) -> Option<Cow<'_, JsonAbi>> {
552        self.get_contract_bytecode().abi
553    }
554
555    /// Returns the `sourceMap` of the creation bytecode
556    ///
557    /// Returns `None` if no `sourceMap` string was included in the compiler output
558    /// Returns `Some(Err)` if parsing the sourcemap failed
559    fn get_source_map(&self) -> Option<std::result::Result<SourceMap, SyntaxError>> {
560        self.get_bytecode()?.source_map()
561    }
562
563    /// Returns the creation bytecode `sourceMap` as str if it was included in the compiler output
564    fn get_source_map_str(&self) -> Option<Cow<'_, str>> {
565        match self.get_bytecode()? {
566            Cow::Borrowed(code) => code.source_map.as_deref().map(Cow::Borrowed),
567            Cow::Owned(code) => code.source_map.map(Cow::Owned),
568        }
569    }
570
571    /// Returns the `sourceMap` of the runtime bytecode
572    ///
573    /// Returns `None` if no `sourceMap` string was included in the compiler output
574    /// Returns `Some(Err)` if parsing the sourcemap failed
575    fn get_source_map_deployed(&self) -> Option<std::result::Result<SourceMap, SyntaxError>> {
576        self.get_deployed_bytecode()?.source_map()
577    }
578
579    /// Returns the runtime bytecode `sourceMap` as str if it was included in the compiler output
580    fn get_source_map_deployed_str(&self) -> Option<Cow<'_, str>> {
581        match self.get_deployed_bytecode()? {
582            Cow::Borrowed(code) => code.bytecode.as_ref()?.source_map.as_deref().map(Cow::Borrowed),
583            Cow::Owned(code) => code.bytecode?.source_map.map(Cow::Owned),
584        }
585    }
586}
587
588impl<T> Artifact for T
589where
590    T: Into<CompactContractBytecode> + Into<CompactContract>,
591    for<'a> &'a T: Into<CompactContractBytecodeCow<'a>>,
592{
593    fn into_inner(self) -> (Option<JsonAbi>, Option<Bytes>) {
594        let artifact = self.into_compact_contract();
595        (artifact.abi, artifact.bin.and_then(|bin| bin.into_bytes()))
596    }
597
598    fn into_compact_contract(self) -> CompactContract {
599        self.into()
600    }
601
602    fn into_contract_bytecode(self) -> CompactContractBytecode {
603        self.into()
604    }
605
606    fn into_parts(self) -> (Option<JsonAbi>, Option<Bytes>, Option<Bytes>) {
607        self.into_compact_contract().into_parts()
608    }
609
610    fn get_contract_bytecode(&self) -> CompactContractBytecodeCow<'_> {
611        self.into()
612    }
613}
614
615/// Handler invoked with the output of `solc`
616///
617/// Implementers of this trait are expected to take care of [`crate::compilers::CompilerContract`]
618/// to [`crate::ArtifactOutput::Artifact`] conversion and how that `Artifact` type is stored on
619/// disk, this includes artifact file location and naming.
620///
621/// Depending on the [`crate::Project`] contracts and their compatible versions,
622/// The project compiler may invoke different `solc` executables on the same
623/// solidity file leading to multiple [`crate::CompilerOutput`]s for the same `.sol` file.
624/// In addition to the `solidity file` to `contract` relationship (1-N*)
625/// [`crate::VersionedContracts`] also tracks the `contract` to (`artifact` + `solc version`)
626/// relationship (1-N+).
627pub trait ArtifactOutput {
628    /// Represents the artifact that will be stored for a `Contract`
629    type Artifact: Artifact + DeserializeOwned + Serialize + fmt::Debug + Send + Sync;
630    type CompilerContract: CompilerContract;
631
632    /// Handle the aggregated set of compiled contracts from the solc [`crate::CompilerOutput`].
633    ///
634    /// This will be invoked with all aggregated contracts from (multiple) solc `CompilerOutput`.
635    /// See [`crate::AggregatedCompilerOutput`]
636    fn on_output<L>(
637        &self,
638        contracts: &VersionedContracts<Self::CompilerContract>,
639        sources: &VersionedSourceFiles,
640        layout: &ProjectPathsConfig<L>,
641        ctx: OutputContext<'_>,
642        primary_profiles: &HashMap<PathBuf, &str>,
643    ) -> Result<Artifacts<Self::Artifact>> {
644        let mut artifacts =
645            self.output_to_artifacts(contracts, sources, ctx, layout, primary_profiles);
646        fs::create_dir_all(&layout.artifacts)
647            .map_err(|err| SolcIoError::new(err, &layout.artifacts))?;
648
649        artifacts.join_all(&layout.artifacts);
650        artifacts.write_all()?;
651
652        self.handle_artifacts(contracts, &artifacts)?;
653
654        Ok(artifacts)
655    }
656
657    /// Invoked after artifacts has been written to disk for additional processing.
658    fn handle_artifacts(
659        &self,
660        _contracts: &VersionedContracts<Self::CompilerContract>,
661        _artifacts: &Artifacts<Self::Artifact>,
662    ) -> Result<()> {
663        Ok(())
664    }
665
666    /// Returns the file name for the contract's artifact
667    /// `Greeter.json`
668    fn output_file_name(
669        name: &str,
670        version: &Version,
671        profile: &str,
672        with_version: bool,
673        with_profile: bool,
674    ) -> PathBuf {
675        let mut name = name.to_string();
676        if with_version {
677            name.push_str(&format!(".{}.{}.{}", version.major, version.minor, version.patch));
678        }
679        if with_profile {
680            name.push_str(&format!(".{profile}"));
681        }
682        name.push_str(".json");
683        name.into()
684    }
685
686    /// Returns the appropriate file name for the conflicting file.
687    ///
688    /// This should ensure that the resulting `PathBuf` is conflict free, which could be possible if
689    /// there are two separate contract files (in different folders) that contain the same contract:
690    ///
691    /// `src/A.sol::A`
692    /// `src/nested/A.sol::A`
693    ///
694    /// Which would result in the same `PathBuf` if only the file and contract name is taken into
695    /// account, [`Self::output_file`].
696    ///
697    /// This return a unique output file
698    fn conflict_free_output_file(
699        already_taken: &HashSet<String>,
700        conflict: PathBuf,
701        contract_file: &Path,
702        artifacts_folder: &Path,
703    ) -> PathBuf {
704        let mut rel_candidate = conflict;
705        if let Ok(stripped) = rel_candidate.strip_prefix(artifacts_folder) {
706            rel_candidate = stripped.to_path_buf();
707        }
708        #[allow(clippy::redundant_clone)] // false positive
709        let mut candidate = rel_candidate.clone();
710        let mut current_parent = contract_file.parent();
711
712        while let Some(parent_name) = current_parent.and_then(|f| f.file_name()) {
713            // this is problematic if both files are absolute
714            candidate = Path::new(parent_name).join(&candidate);
715            let out_path = artifacts_folder.join(&candidate);
716            if !already_taken.contains(&out_path.to_slash_lossy().to_lowercase()) {
717                trace!("found alternative output file={:?} for {:?}", out_path, contract_file);
718                return out_path;
719            }
720            current_parent = current_parent.and_then(|f| f.parent());
721        }
722
723        // this means we haven't found an alternative yet, which shouldn't actually happen since
724        // `contract_file` are unique, but just to be safe, handle this case in which case
725        // we simply numerate the parent folder
726
727        trace!("no conflict free output file found after traversing the file");
728
729        let mut num = 1;
730
731        loop {
732            // this will attempt to find an alternate path by numerating the first component in the
733            // path: `<root>+_<num>/....sol`
734            let mut components = rel_candidate.components();
735            let first = components.next().expect("path not empty");
736            let name = first.as_os_str();
737            let mut numerated = OsString::with_capacity(name.len() + 2);
738            numerated.push(name);
739            numerated.push("_");
740            numerated.push(num.to_string());
741
742            let candidate: PathBuf = Some(numerated.as_os_str())
743                .into_iter()
744                .chain(components.map(|c| c.as_os_str()))
745                .collect();
746            if !already_taken.contains(&candidate.to_slash_lossy().to_lowercase()) {
747                trace!("found alternative output file={:?} for {:?}", candidate, contract_file);
748                return candidate;
749            }
750
751            num += 1;
752        }
753    }
754
755    /// Returns the path to the contract's artifact location based on the contract's file and name
756    ///
757    /// This returns `contract.sol/contract.json` by default
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        contract_file
767            .file_name()
768            .map(Path::new)
769            .map(|p| {
770                p.join(Self::output_file_name(name, version, profile, with_version, with_profile))
771            })
772            .unwrap_or_else(|| {
773                Self::output_file_name(name, version, profile, with_version, with_profile)
774            })
775    }
776
777    /// The inverse of `contract_file_name`
778    ///
779    /// Expected to return the solidity contract's name derived from the file path
780    /// `sources/Greeter.sol` -> `Greeter`
781    fn contract_name(file: &Path) -> Option<String> {
782        file.file_stem().and_then(|s| s.to_str().map(|s| s.to_string()))
783    }
784
785    /// Read the artifact that's stored at the given path
786    ///
787    /// # Errors
788    ///
789    /// Returns an error if
790    ///     - The file does not exist
791    ///     - The file's content couldn't be deserialized into the `Artifact` type
792    fn read_cached_artifact(path: &Path) -> Result<Self::Artifact> {
793        utils::read_json_file(path)
794    }
795
796    /// Read the cached artifacts that are located the paths the iterator yields
797    ///
798    /// See [`Self::read_cached_artifact()`]
799    fn read_cached_artifacts<T, I>(files: I) -> Result<BTreeMap<PathBuf, Self::Artifact>>
800    where
801        I: IntoIterator<Item = T>,
802        T: Into<PathBuf>,
803    {
804        let mut artifacts = BTreeMap::default();
805        for path in files {
806            let path = path.into();
807            let artifact = Self::read_cached_artifact(&path)?;
808            artifacts.insert(path, artifact);
809        }
810        Ok(artifacts)
811    }
812
813    /// Convert a contract to the artifact type
814    ///
815    /// This is the core conversion function that takes care of converting a `Contract` into the
816    /// associated `Artifact` type.
817    /// The `SourceFile` is also provided
818    fn contract_to_artifact(
819        &self,
820        _file: &Path,
821        _name: &str,
822        contract: Self::CompilerContract,
823        source_file: Option<&SourceFile>,
824    ) -> Self::Artifact;
825
826    /// Generates a path for an artifact based on already taken paths by either cached or compiled
827    /// artifacts.
828    #[allow(clippy::too_many_arguments)]
829    fn get_artifact_path(
830        ctx: &OutputContext<'_>,
831        already_taken: &HashSet<String>,
832        file: &Path,
833        name: &str,
834        artifacts_folder: &Path,
835        version: &Version,
836        profile: &str,
837        with_version: bool,
838        with_profile: bool,
839    ) -> PathBuf {
840        // if an artifact for the contract already exists (from a previous compile job)
841        // we reuse the path, this will make sure that even if there are conflicting
842        // files (files for witch `T::output_file()` would return the same path) we use
843        // consistent output paths
844        if let Some(existing_artifact) = ctx.existing_artifact(file, name, version, profile) {
845            trace!("use existing artifact file {:?}", existing_artifact,);
846            existing_artifact.to_path_buf()
847        } else {
848            let path = Self::output_file(file, name, version, profile, with_version, with_profile);
849
850            let path = artifacts_folder.join(path);
851
852            if already_taken.contains(&path.to_slash_lossy().to_lowercase()) {
853                // preventing conflict
854                Self::conflict_free_output_file(already_taken, path, file, artifacts_folder)
855            } else {
856                path
857            }
858        }
859    }
860
861    /// Convert the compiler output into a set of artifacts
862    ///
863    /// **Note:** This does only convert, but _NOT_ write the artifacts to disk, See
864    /// [`Self::on_output()`]
865    fn output_to_artifacts<C>(
866        &self,
867        contracts: &VersionedContracts<Self::CompilerContract>,
868        sources: &VersionedSourceFiles,
869        ctx: OutputContext<'_>,
870        layout: &ProjectPathsConfig<C>,
871        primary_profiles: &HashMap<PathBuf, &str>,
872    ) -> Artifacts<Self::Artifact> {
873        let mut artifacts = ArtifactsMap::new();
874
875        // this tracks all the `SourceFile`s that we successfully mapped to a contract
876        let mut non_standalone_sources = HashSet::new();
877
878        // prepopulate taken paths set with cached artifacts
879        let mut taken_paths_lowercase = ctx
880            .existing_artifacts
881            .values()
882            .flat_map(|artifacts| artifacts.values())
883            .flat_map(|artifacts| artifacts.values())
884            .flat_map(|artifacts| artifacts.values())
885            .map(|a| a.path.to_slash_lossy().to_lowercase())
886            .collect::<HashSet<_>>();
887
888        let mut files = contracts.keys().collect::<Vec<_>>();
889        // Iterate starting with top-most files to ensure that they get the shortest paths.
890        files.sort_by(|&file1, &file2| {
891            (file1.components().count(), file1).cmp(&(file2.components().count(), file2))
892        });
893        for file in files {
894            for (name, versioned_contracts) in &contracts[file] {
895                let unique_versions =
896                    versioned_contracts.iter().map(|c| &c.version).collect::<HashSet<_>>();
897                let unique_profiles =
898                    versioned_contracts.iter().map(|c| &c.profile).collect::<HashSet<_>>();
899                let primary_profile = primary_profiles.get(file);
900
901                for contract in versioned_contracts {
902                    non_standalone_sources.insert(file);
903
904                    // track `SourceFile`s that can be mapped to contracts
905                    let source_file = sources.find_file_and_version(file, &contract.version);
906
907                    let artifact_path = Self::get_artifact_path(
908                        &ctx,
909                        &taken_paths_lowercase,
910                        file,
911                        name,
912                        layout.artifacts.as_path(),
913                        &contract.version,
914                        &contract.profile,
915                        unique_versions.len() > 1,
916                        unique_profiles.len() > 1
917                            && primary_profile.is_none_or(|p| p != &contract.profile),
918                    );
919
920                    taken_paths_lowercase.insert(artifact_path.to_slash_lossy().to_lowercase());
921
922                    trace!(
923                        "use artifact file {:?} for contract file {} {}",
924                        artifact_path,
925                        file.display(),
926                        contract.version
927                    );
928
929                    let artifact = self.contract_to_artifact(
930                        file,
931                        name,
932                        contract.contract.clone(),
933                        source_file,
934                    );
935
936                    let artifact = ArtifactFile {
937                        artifact,
938                        file: artifact_path,
939                        version: contract.version.clone(),
940                        build_id: contract.build_id.clone(),
941                        profile: contract.profile.clone(),
942                    };
943
944                    artifacts
945                        .entry(file.clone())
946                        .or_default()
947                        .entry(name.clone())
948                        .or_default()
949                        .push(artifact);
950                }
951            }
952        }
953
954        // extend with standalone source files and convert them to artifacts
955        // this is unfortunately necessary, so we can "mock" `Artifacts` for solidity files without
956        // any contract definition, which are not included in the `CompilerOutput` but we want to
957        // create Artifacts for them regardless
958        for (file, sources) in sources.as_ref() {
959            let unique_versions = sources.iter().map(|s| &s.version).collect::<HashSet<_>>();
960            let unique_profiles = sources.iter().map(|s| &s.profile).collect::<HashSet<_>>();
961            for source in sources {
962                if !non_standalone_sources.contains(file) {
963                    // scan the ast as a safe measure to ensure this file does not include any
964                    // source units
965                    // there's also no need to create a standalone artifact for source files that
966                    // don't contain an ast
967                    if source.source_file.ast.is_none()
968                        || source.source_file.contains_contract_definition()
969                    {
970                        continue;
971                    }
972
973                    // we use file and file stem
974                    if let Some(name) = Path::new(file).file_stem().and_then(|stem| stem.to_str())
975                        && let Some(artifact) =
976                            self.standalone_source_file_to_artifact(file, source)
977                    {
978                        let artifact_path = Self::get_artifact_path(
979                            &ctx,
980                            &taken_paths_lowercase,
981                            file,
982                            name,
983                            &layout.artifacts,
984                            &source.version,
985                            &source.profile,
986                            unique_versions.len() > 1,
987                            unique_profiles.len() > 1,
988                        );
989
990                        taken_paths_lowercase.insert(artifact_path.to_slash_lossy().to_lowercase());
991
992                        artifacts
993                            .entry(file.clone())
994                            .or_default()
995                            .entry(name.to_string())
996                            .or_default()
997                            .push(ArtifactFile {
998                                artifact,
999                                file: artifact_path,
1000                                version: source.version.clone(),
1001                                build_id: source.build_id.clone(),
1002                                profile: source.profile.clone(),
1003                            });
1004                    }
1005                }
1006            }
1007        }
1008
1009        Artifacts(artifacts)
1010    }
1011
1012    /// This converts a `SourceFile` that doesn't contain _any_ contract definitions (interfaces,
1013    /// contracts, libraries) to an artifact.
1014    ///
1015    /// We do this because not all `SourceFile`s emitted by solc have at least 1 corresponding entry
1016    /// in the `contracts`
1017    /// section of the solc output. For example for an `errors.sol` that only contains custom error
1018    /// definitions and no contract, no `Contract` object will be generated by solc. However, we
1019    /// still want to emit an `Artifact` for that file that may include the `ast`, docs etc.,
1020    /// because other tools depend on this, such as slither.
1021    fn standalone_source_file_to_artifact(
1022        &self,
1023        _path: &Path,
1024        _file: &VersionedSourceFile,
1025    ) -> Option<Self::Artifact>;
1026
1027    /// Handler allowing artifacts handler to enforce artifact recompilation.
1028    fn is_dirty(&self, _artifact_file: &ArtifactFile<Self::Artifact>) -> Result<bool> {
1029        Ok(false)
1030    }
1031
1032    /// Invoked with all artifacts that were not recompiled.
1033    fn handle_cached_artifacts(&self, _artifacts: &Artifacts<Self::Artifact>) -> Result<()> {
1034        Ok(())
1035    }
1036}
1037
1038/// Additional context to use during [`ArtifactOutput::on_output()`]
1039#[derive(Clone, Debug, Default)]
1040#[non_exhaustive]
1041pub struct OutputContext<'a> {
1042    /// Cache file of the project or empty if no caching is enabled
1043    ///
1044    /// This context is required for partially cached recompile with conflicting files, so that we
1045    /// can use the same adjusted output path for conflicting files like:
1046    ///
1047    /// ```text
1048    /// src
1049    /// ├── a.sol
1050    /// └── inner
1051    ///     └── a.sol
1052    /// ```
1053    pub existing_artifacts: BTreeMap<&'a Path, &'a CachedArtifacts>,
1054}
1055
1056// === impl OutputContext
1057
1058impl<'a> OutputContext<'a> {
1059    /// Create a new context with the given cache file
1060    pub fn new<S>(cache: &'a CompilerCache<S>) -> Self {
1061        let existing_artifacts = cache
1062            .files
1063            .iter()
1064            .map(|(file, entry)| (file.as_path(), &entry.artifacts))
1065            .collect::<BTreeMap<_, _>>();
1066
1067        Self { existing_artifacts }
1068    }
1069
1070    /// Returns the path of the already existing artifact for the `contract` of the `file` compiled
1071    /// with the `version`.
1072    ///
1073    /// Returns `None` if no file exists
1074    pub fn existing_artifact(
1075        &self,
1076        file: &Path,
1077        contract: &str,
1078        version: &Version,
1079        profile: &str,
1080    ) -> Option<&Path> {
1081        self.existing_artifacts
1082            .get(file)
1083            .and_then(|contracts| contracts.get(contract))
1084            .and_then(|versions| versions.get(version))
1085            .and_then(|profiles| profiles.get(profile))
1086            .map(|a| a.path.as_path())
1087    }
1088}
1089
1090/// An `Artifact` implementation that uses a compact representation
1091///
1092/// Creates a single json artifact with
1093/// ```json
1094///  {
1095///    "abi": [],
1096///    "bytecode": {...},
1097///    "deployedBytecode": {...}
1098///  }
1099/// ```
1100#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1101pub struct MinimalCombinedArtifacts {
1102    _priv: (),
1103}
1104
1105impl ArtifactOutput for MinimalCombinedArtifacts {
1106    type Artifact = CompactContractBytecode;
1107    type CompilerContract = Contract;
1108
1109    fn contract_to_artifact(
1110        &self,
1111        _file: &Path,
1112        _name: &str,
1113        contract: Contract,
1114        _source_file: Option<&SourceFile>,
1115    ) -> Self::Artifact {
1116        Self::Artifact::from(contract)
1117    }
1118
1119    fn standalone_source_file_to_artifact(
1120        &self,
1121        _path: &Path,
1122        _file: &VersionedSourceFile,
1123    ) -> Option<Self::Artifact> {
1124        None
1125    }
1126}
1127
1128/// An Artifacts handler implementation that works the same as `MinimalCombinedArtifacts` but also
1129/// supports reading hardhat artifacts if an initial attempt to deserialize an artifact failed
1130#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1131pub struct MinimalCombinedArtifactsHardhatFallback {
1132    _priv: (),
1133}
1134
1135impl ArtifactOutput for MinimalCombinedArtifactsHardhatFallback {
1136    type Artifact = CompactContractBytecode;
1137    type CompilerContract = Contract;
1138
1139    fn on_output<C>(
1140        &self,
1141        output: &VersionedContracts<Contract>,
1142        sources: &VersionedSourceFiles,
1143        layout: &ProjectPathsConfig<C>,
1144        ctx: OutputContext<'_>,
1145        primary_profiles: &HashMap<PathBuf, &str>,
1146    ) -> Result<Artifacts<Self::Artifact>> {
1147        MinimalCombinedArtifacts::default().on_output(
1148            output,
1149            sources,
1150            layout,
1151            ctx,
1152            primary_profiles,
1153        )
1154    }
1155
1156    fn read_cached_artifact(path: &Path) -> Result<Self::Artifact> {
1157        #[derive(Deserialize)]
1158        #[serde(untagged)]
1159        enum Artifact {
1160            Compact(CompactContractBytecode),
1161            Hardhat(HardhatArtifact),
1162        }
1163
1164        Ok(match utils::read_json_file::<Artifact>(path)? {
1165            Artifact::Compact(c) => c,
1166            Artifact::Hardhat(h) => h.into_contract_bytecode(),
1167        })
1168    }
1169
1170    fn contract_to_artifact(
1171        &self,
1172        file: &Path,
1173        name: &str,
1174        contract: Contract,
1175        source_file: Option<&SourceFile>,
1176    ) -> Self::Artifact {
1177        MinimalCombinedArtifacts::default().contract_to_artifact(file, name, contract, source_file)
1178    }
1179
1180    fn standalone_source_file_to_artifact(
1181        &self,
1182        path: &Path,
1183        file: &VersionedSourceFile,
1184    ) -> Option<Self::Artifact> {
1185        MinimalCombinedArtifacts::default().standalone_source_file_to_artifact(path, file)
1186    }
1187}
1188
1189#[cfg(test)]
1190mod tests {
1191    use super::*;
1192
1193    #[test]
1194    fn is_artifact() {
1195        fn assert_artifact<T: Artifact>() {}
1196
1197        assert_artifact::<CompactContractBytecode>();
1198        assert_artifact::<serde_json::Value>();
1199    }
1200
1201    #[test]
1202    fn can_find_alternate_paths() {
1203        let mut already_taken = HashSet::new();
1204
1205        let file = Path::new("v1/tokens/Greeter.sol");
1206        let conflict = PathBuf::from("out/Greeter.sol/Greeter.json");
1207        let artifacts_folder = Path::new("out");
1208
1209        let alternative = ConfigurableArtifacts::conflict_free_output_file(
1210            &already_taken,
1211            conflict.clone(),
1212            file,
1213            artifacts_folder,
1214        );
1215        assert_eq!(alternative.to_slash_lossy(), "out/tokens/Greeter.sol/Greeter.json");
1216
1217        already_taken.insert("out/tokens/Greeter.sol/Greeter.json".to_lowercase());
1218        let alternative = ConfigurableArtifacts::conflict_free_output_file(
1219            &already_taken,
1220            conflict.clone(),
1221            file,
1222            artifacts_folder,
1223        );
1224        assert_eq!(alternative.to_slash_lossy(), "out/v1/tokens/Greeter.sol/Greeter.json");
1225
1226        already_taken.insert("out/v1/tokens/Greeter.sol/Greeter.json".to_lowercase());
1227        let alternative = ConfigurableArtifacts::conflict_free_output_file(
1228            &already_taken,
1229            conflict,
1230            file,
1231            artifacts_folder,
1232        );
1233        assert_eq!(alternative, PathBuf::from("Greeter.sol_1/Greeter.json"));
1234    }
1235
1236    #[test]
1237    fn can_find_alternate_path_conflict() {
1238        let mut already_taken = HashSet::new();
1239
1240        let file = "/Users/carter/dev/goldfinch/mono/packages/protocol/test/forge/mainnet/utils/BaseMainnetForkingTest.t.sol";
1241        let conflict = PathBuf::from(
1242            "/Users/carter/dev/goldfinch/mono/packages/protocol/artifacts/BaseMainnetForkingTest.t.sol/BaseMainnetForkingTest.json",
1243        );
1244        already_taken.insert("/Users/carter/dev/goldfinch/mono/packages/protocol/artifacts/BaseMainnetForkingTest.t.sol/BaseMainnetForkingTest.json".into());
1245
1246        let alternative = ConfigurableArtifacts::conflict_free_output_file(
1247            &already_taken,
1248            conflict,
1249            file.as_ref(),
1250            "/Users/carter/dev/goldfinch/mono/packages/protocol/artifacts".as_ref(),
1251        );
1252
1253        assert_eq!(
1254            alternative.to_slash_lossy(),
1255            "/Users/carter/dev/goldfinch/mono/packages/protocol/artifacts/utils/BaseMainnetForkingTest.t.sol/BaseMainnetForkingTest.json"
1256        );
1257    }
1258
1259    fn assert_artifact<T: crate::Artifact>() {}
1260
1261    #[test]
1262    fn test() {
1263        assert_artifact::<CompactContractBytecode>();
1264        assert_artifact::<CompactContractBytecodeCow<'static>>();
1265    }
1266
1267    #[test]
1268    fn deployed_source_map_string_uses_runtime_bytecode() {
1269        let artifact = CompactContractBytecode {
1270            abi: None,
1271            bytecode: Some(CompactBytecode {
1272                object: BytecodeObject::Bytecode(Default::default()),
1273                source_map: Some("creation-map".to_string()),
1274                link_references: BTreeMap::new(),
1275            }),
1276            deployed_bytecode: Some(CompactDeployedBytecode {
1277                bytecode: Some(CompactBytecode {
1278                    object: BytecodeObject::Bytecode(Default::default()),
1279                    source_map: Some("runtime-map".to_string()),
1280                    link_references: BTreeMap::new(),
1281                }),
1282                immutable_references: BTreeMap::new(),
1283            }),
1284        };
1285
1286        assert_eq!(artifact.get_source_map_deployed_str().as_deref(), Some("runtime-map"));
1287    }
1288}