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