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).map_err(|err| {
629            error!(dir=?layout.artifacts, "Failed to create artifacts folder");
630            SolcIoError::new(err, &layout.artifacts)
631        })?;
632
633        artifacts.join_all(&layout.artifacts);
634        artifacts.write_all()?;
635
636        self.handle_artifacts(contracts, &artifacts)?;
637
638        Ok(artifacts)
639    }
640
641    /// Invoked after artifacts has been written to disk for additional processing.
642    fn handle_artifacts(
643        &self,
644        _contracts: &VersionedContracts<Self::CompilerContract>,
645        _artifacts: &Artifacts<Self::Artifact>,
646    ) -> Result<()> {
647        Ok(())
648    }
649
650    /// Returns the file name for the contract's artifact
651    /// `Greeter.json`
652    fn output_file_name(
653        name: &str,
654        version: &Version,
655        profile: &str,
656        with_version: bool,
657        with_profile: bool,
658    ) -> PathBuf {
659        let mut name = name.to_string();
660        if with_version {
661            name.push_str(&format!(".{}.{}.{}", version.major, version.minor, version.patch));
662        }
663        if with_profile {
664            name.push_str(&format!(".{profile}"));
665        }
666        name.push_str(".json");
667        name.into()
668    }
669
670    /// Returns the appropriate file name for the conflicting file.
671    ///
672    /// This should ensure that the resulting `PathBuf` is conflict free, which could be possible if
673    /// there are two separate contract files (in different folders) that contain the same contract:
674    ///
675    /// `src/A.sol::A`
676    /// `src/nested/A.sol::A`
677    ///
678    /// Which would result in the same `PathBuf` if only the file and contract name is taken into
679    /// account, [`Self::output_file`].
680    ///
681    /// This return a unique output file
682    fn conflict_free_output_file(
683        already_taken: &HashSet<String>,
684        conflict: PathBuf,
685        contract_file: &Path,
686        artifacts_folder: &Path,
687    ) -> PathBuf {
688        let mut rel_candidate = conflict;
689        if let Ok(stripped) = rel_candidate.strip_prefix(artifacts_folder) {
690            rel_candidate = stripped.to_path_buf();
691        }
692        #[allow(clippy::redundant_clone)] // false positive
693        let mut candidate = rel_candidate.clone();
694        let mut current_parent = contract_file.parent();
695
696        while let Some(parent_name) = current_parent.and_then(|f| f.file_name()) {
697            // this is problematic if both files are absolute
698            candidate = Path::new(parent_name).join(&candidate);
699            let out_path = artifacts_folder.join(&candidate);
700            if !already_taken.contains(&out_path.to_slash_lossy().to_lowercase()) {
701                trace!("found alternative output file={:?} for {:?}", out_path, contract_file);
702                return out_path;
703            }
704            current_parent = current_parent.and_then(|f| f.parent());
705        }
706
707        // this means we haven't found an alternative yet, which shouldn't actually happen since
708        // `contract_file` are unique, but just to be safe, handle this case in which case
709        // we simply numerate the parent folder
710
711        trace!("no conflict free output file found after traversing the file");
712
713        let mut num = 1;
714
715        loop {
716            // this will attempt to find an alternate path by numerating the first component in the
717            // path: `<root>+_<num>/....sol`
718            let mut components = rel_candidate.components();
719            let first = components.next().expect("path not empty");
720            let name = first.as_os_str();
721            let mut numerated = OsString::with_capacity(name.len() + 2);
722            numerated.push(name);
723            numerated.push("_");
724            numerated.push(num.to_string());
725
726            let candidate: PathBuf = Some(numerated.as_os_str())
727                .into_iter()
728                .chain(components.map(|c| c.as_os_str()))
729                .collect();
730            if !already_taken.contains(&candidate.to_slash_lossy().to_lowercase()) {
731                trace!("found alternative output file={:?} for {:?}", candidate, contract_file);
732                return candidate;
733            }
734
735            num += 1;
736        }
737    }
738
739    /// Returns the path to the contract's artifact location based on the contract's file and name
740    ///
741    /// This returns `contract.sol/contract.json` by default
742    fn output_file(
743        contract_file: &Path,
744        name: &str,
745        version: &Version,
746        profile: &str,
747        with_version: bool,
748        with_profile: bool,
749    ) -> PathBuf {
750        contract_file
751            .file_name()
752            .map(Path::new)
753            .map(|p| {
754                p.join(Self::output_file_name(name, version, profile, with_version, with_profile))
755            })
756            .unwrap_or_else(|| {
757                Self::output_file_name(name, version, profile, with_version, with_profile)
758            })
759    }
760
761    /// The inverse of `contract_file_name`
762    ///
763    /// Expected to return the solidity contract's name derived from the file path
764    /// `sources/Greeter.sol` -> `Greeter`
765    fn contract_name(file: &Path) -> Option<String> {
766        file.file_stem().and_then(|s| s.to_str().map(|s| s.to_string()))
767    }
768
769    /// Read the artifact that's stored at the given path
770    ///
771    /// # Errors
772    ///
773    /// Returns an error if
774    ///     - The file does not exist
775    ///     - The file's content couldn't be deserialized into the `Artifact` type
776    fn read_cached_artifact(path: &Path) -> Result<Self::Artifact> {
777        utils::read_json_file(path)
778    }
779
780    /// Read the cached artifacts that are located the paths the iterator yields
781    ///
782    /// See [`Self::read_cached_artifact()`]
783    fn read_cached_artifacts<T, I>(files: I) -> Result<BTreeMap<PathBuf, Self::Artifact>>
784    where
785        I: IntoIterator<Item = T>,
786        T: Into<PathBuf>,
787    {
788        let mut artifacts = BTreeMap::default();
789        for path in files.into_iter() {
790            let path = path.into();
791            let artifact = Self::read_cached_artifact(&path)?;
792            artifacts.insert(path, artifact);
793        }
794        Ok(artifacts)
795    }
796
797    /// Convert a contract to the artifact type
798    ///
799    /// This is the core conversion function that takes care of converting a `Contract` into the
800    /// associated `Artifact` type.
801    /// The `SourceFile` is also provided
802    fn contract_to_artifact(
803        &self,
804        _file: &Path,
805        _name: &str,
806        contract: Self::CompilerContract,
807        source_file: Option<&SourceFile>,
808    ) -> Self::Artifact;
809
810    /// Generates a path for an artifact based on already taken paths by either cached or compiled
811    /// artifacts.
812    #[allow(clippy::too_many_arguments)]
813    fn get_artifact_path(
814        ctx: &OutputContext<'_>,
815        already_taken: &HashSet<String>,
816        file: &Path,
817        name: &str,
818        artifacts_folder: &Path,
819        version: &Version,
820        profile: &str,
821        with_version: bool,
822        with_profile: bool,
823    ) -> PathBuf {
824        // if an artifact for the contract already exists (from a previous compile job)
825        // we reuse the path, this will make sure that even if there are conflicting
826        // files (files for witch `T::output_file()` would return the same path) we use
827        // consistent output paths
828        if let Some(existing_artifact) = ctx.existing_artifact(file, name, version, profile) {
829            trace!("use existing artifact file {:?}", existing_artifact,);
830            existing_artifact.to_path_buf()
831        } else {
832            let path = Self::output_file(file, name, version, profile, with_version, with_profile);
833
834            let path = artifacts_folder.join(path);
835
836            if already_taken.contains(&path.to_slash_lossy().to_lowercase()) {
837                // preventing conflict
838                Self::conflict_free_output_file(already_taken, path, file, artifacts_folder)
839            } else {
840                path
841            }
842        }
843    }
844
845    /// Convert the compiler output into a set of artifacts
846    ///
847    /// **Note:** This does only convert, but _NOT_ write the artifacts to disk, See
848    /// [`Self::on_output()`]
849    fn output_to_artifacts<C>(
850        &self,
851        contracts: &VersionedContracts<Self::CompilerContract>,
852        sources: &VersionedSourceFiles,
853        ctx: OutputContext<'_>,
854        layout: &ProjectPathsConfig<C>,
855        primary_profiles: &HashMap<PathBuf, &str>,
856    ) -> Artifacts<Self::Artifact> {
857        let mut artifacts = ArtifactsMap::new();
858
859        // this tracks all the `SourceFile`s that we successfully mapped to a contract
860        let mut non_standalone_sources = HashSet::new();
861
862        // prepopulate taken paths set with cached artifacts
863        let mut taken_paths_lowercase = ctx
864            .existing_artifacts
865            .values()
866            .flat_map(|artifacts| artifacts.values())
867            .flat_map(|artifacts| artifacts.values())
868            .flat_map(|artifacts| artifacts.values())
869            .map(|a| a.path.to_slash_lossy().to_lowercase())
870            .collect::<HashSet<_>>();
871
872        let mut files = contracts.keys().collect::<Vec<_>>();
873        // Iterate starting with top-most files to ensure that they get the shortest paths.
874        files.sort_by(|&file1, &file2| {
875            (file1.components().count(), file1).cmp(&(file2.components().count(), file2))
876        });
877        for file in files {
878            for (name, versioned_contracts) in &contracts[file] {
879                let unique_versions =
880                    versioned_contracts.iter().map(|c| &c.version).collect::<HashSet<_>>();
881                let unique_profiles =
882                    versioned_contracts.iter().map(|c| &c.profile).collect::<HashSet<_>>();
883                let primary_profile = primary_profiles.get(file);
884
885                for contract in versioned_contracts {
886                    non_standalone_sources.insert(file);
887
888                    // track `SourceFile`s that can be mapped to contracts
889                    let source_file = sources.find_file_and_version(file, &contract.version);
890
891                    let artifact_path = Self::get_artifact_path(
892                        &ctx,
893                        &taken_paths_lowercase,
894                        file,
895                        name,
896                        layout.artifacts.as_path(),
897                        &contract.version,
898                        &contract.profile,
899                        unique_versions.len() > 1,
900                        unique_profiles.len() > 1
901                            && primary_profile.is_none_or(|p| p != &contract.profile),
902                    );
903
904                    taken_paths_lowercase.insert(artifact_path.to_slash_lossy().to_lowercase());
905
906                    trace!(
907                        "use artifact file {:?} for contract file {} {}",
908                        artifact_path,
909                        file.display(),
910                        contract.version
911                    );
912
913                    let artifact = self.contract_to_artifact(
914                        file,
915                        name,
916                        contract.contract.clone(),
917                        source_file,
918                    );
919
920                    let artifact = ArtifactFile {
921                        artifact,
922                        file: artifact_path,
923                        version: contract.version.clone(),
924                        build_id: contract.build_id.clone(),
925                        profile: contract.profile.clone(),
926                    };
927
928                    artifacts
929                        .entry(file.to_path_buf())
930                        .or_default()
931                        .entry(name.to_string())
932                        .or_default()
933                        .push(artifact);
934                }
935            }
936        }
937
938        // extend with standalone source files and convert them to artifacts
939        // this is unfortunately necessary, so we can "mock" `Artifacts` for solidity files without
940        // any contract definition, which are not included in the `CompilerOutput` but we want to
941        // create Artifacts for them regardless
942        for (file, sources) in sources.as_ref().iter() {
943            let unique_versions = sources.iter().map(|s| &s.version).collect::<HashSet<_>>();
944            let unique_profiles = sources.iter().map(|s| &s.profile).collect::<HashSet<_>>();
945            for source in sources {
946                if !non_standalone_sources.contains(file) {
947                    // scan the ast as a safe measure to ensure this file does not include any
948                    // source units
949                    // there's also no need to create a standalone artifact for source files that
950                    // don't contain an ast
951                    if source.source_file.ast.is_none()
952                        || source.source_file.contains_contract_definition()
953                    {
954                        continue;
955                    }
956
957                    // we use file and file stem
958                    if let Some(name) = Path::new(file).file_stem().and_then(|stem| stem.to_str()) {
959                        if let Some(artifact) =
960                            self.standalone_source_file_to_artifact(file, source)
961                        {
962                            let artifact_path = Self::get_artifact_path(
963                                &ctx,
964                                &taken_paths_lowercase,
965                                file,
966                                name,
967                                &layout.artifacts,
968                                &source.version,
969                                &source.profile,
970                                unique_versions.len() > 1,
971                                unique_profiles.len() > 1,
972                            );
973
974                            taken_paths_lowercase
975                                .insert(artifact_path.to_slash_lossy().to_lowercase());
976
977                            artifacts
978                                .entry(file.clone())
979                                .or_default()
980                                .entry(name.to_string())
981                                .or_default()
982                                .push(ArtifactFile {
983                                    artifact,
984                                    file: artifact_path,
985                                    version: source.version.clone(),
986                                    build_id: source.build_id.clone(),
987                                    profile: source.profile.clone(),
988                                });
989                        }
990                    }
991                }
992            }
993        }
994
995        Artifacts(artifacts)
996    }
997
998    /// This converts a `SourceFile` that doesn't contain _any_ contract definitions (interfaces,
999    /// contracts, libraries) to an artifact.
1000    ///
1001    /// We do this because not all `SourceFile`s emitted by solc have at least 1 corresponding entry
1002    /// in the `contracts`
1003    /// section of the solc output. For example for an `errors.sol` that only contains custom error
1004    /// definitions and no contract, no `Contract` object will be generated by solc. However, we
1005    /// still want to emit an `Artifact` for that file that may include the `ast`, docs etc.,
1006    /// because other tools depend on this, such as slither.
1007    fn standalone_source_file_to_artifact(
1008        &self,
1009        _path: &Path,
1010        _file: &VersionedSourceFile,
1011    ) -> Option<Self::Artifact>;
1012
1013    /// Handler allowing artifacts handler to enforce artifact recompilation.
1014    fn is_dirty(&self, _artifact_file: &ArtifactFile<Self::Artifact>) -> Result<bool> {
1015        Ok(false)
1016    }
1017
1018    /// Invoked with all artifacts that were not recompiled.
1019    fn handle_cached_artifacts(&self, _artifacts: &Artifacts<Self::Artifact>) -> Result<()> {
1020        Ok(())
1021    }
1022}
1023
1024/// Additional context to use during [`ArtifactOutput::on_output()`]
1025#[derive(Clone, Debug, Default)]
1026#[non_exhaustive]
1027pub struct OutputContext<'a> {
1028    /// Cache file of the project or empty if no caching is enabled
1029    ///
1030    /// This context is required for partially cached recompile with conflicting files, so that we
1031    /// can use the same adjusted output path for conflicting files like:
1032    ///
1033    /// ```text
1034    /// src
1035    /// ├── a.sol
1036    /// └── inner
1037    ///     └── a.sol
1038    /// ```
1039    pub existing_artifacts: BTreeMap<&'a Path, &'a CachedArtifacts>,
1040}
1041
1042// === impl OutputContext
1043
1044impl<'a> OutputContext<'a> {
1045    /// Create a new context with the given cache file
1046    pub fn new<S>(cache: &'a CompilerCache<S>) -> Self {
1047        let existing_artifacts = cache
1048            .files
1049            .iter()
1050            .map(|(file, entry)| (file.as_path(), &entry.artifacts))
1051            .collect::<BTreeMap<_, _>>();
1052
1053        Self { existing_artifacts }
1054    }
1055
1056    /// Returns the path of the already existing artifact for the `contract` of the `file` compiled
1057    /// with the `version`.
1058    ///
1059    /// Returns `None` if no file exists
1060    pub fn existing_artifact(
1061        &self,
1062        file: &Path,
1063        contract: &str,
1064        version: &Version,
1065        profile: &str,
1066    ) -> Option<&Path> {
1067        self.existing_artifacts
1068            .get(file)
1069            .and_then(|contracts| contracts.get(contract))
1070            .and_then(|versions| versions.get(version))
1071            .and_then(|profiles| profiles.get(profile))
1072            .map(|a| a.path.as_path())
1073    }
1074}
1075
1076/// An `Artifact` implementation that uses a compact representation
1077///
1078/// Creates a single json artifact with
1079/// ```json
1080///  {
1081///    "abi": [],
1082///    "bytecode": {...},
1083///    "deployedBytecode": {...}
1084///  }
1085/// ```
1086#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1087pub struct MinimalCombinedArtifacts {
1088    _priv: (),
1089}
1090
1091impl ArtifactOutput for MinimalCombinedArtifacts {
1092    type Artifact = CompactContractBytecode;
1093    type CompilerContract = Contract;
1094
1095    fn contract_to_artifact(
1096        &self,
1097        _file: &Path,
1098        _name: &str,
1099        contract: Contract,
1100        _source_file: Option<&SourceFile>,
1101    ) -> Self::Artifact {
1102        Self::Artifact::from(contract)
1103    }
1104
1105    fn standalone_source_file_to_artifact(
1106        &self,
1107        _path: &Path,
1108        _file: &VersionedSourceFile,
1109    ) -> Option<Self::Artifact> {
1110        None
1111    }
1112}
1113
1114/// An Artifacts handler implementation that works the same as `MinimalCombinedArtifacts` but also
1115/// supports reading hardhat artifacts if an initial attempt to deserialize an artifact failed
1116#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1117pub struct MinimalCombinedArtifactsHardhatFallback {
1118    _priv: (),
1119}
1120
1121impl ArtifactOutput for MinimalCombinedArtifactsHardhatFallback {
1122    type Artifact = CompactContractBytecode;
1123    type CompilerContract = Contract;
1124
1125    fn on_output<C>(
1126        &self,
1127        output: &VersionedContracts<Contract>,
1128        sources: &VersionedSourceFiles,
1129        layout: &ProjectPathsConfig<C>,
1130        ctx: OutputContext<'_>,
1131        primary_profiles: &HashMap<PathBuf, &str>,
1132    ) -> Result<Artifacts<Self::Artifact>> {
1133        MinimalCombinedArtifacts::default().on_output(
1134            output,
1135            sources,
1136            layout,
1137            ctx,
1138            primary_profiles,
1139        )
1140    }
1141
1142    fn read_cached_artifact(path: &Path) -> Result<Self::Artifact> {
1143        let content = fs::read_to_string(path).map_err(|err| SolcError::io(err, path))?;
1144        if let Ok(a) = serde_json::from_str(&content) {
1145            Ok(a)
1146        } else {
1147            error!("Failed to deserialize compact artifact");
1148            trace!("Fallback to hardhat artifact deserialization");
1149            let artifact = serde_json::from_str::<HardhatArtifact>(&content)?;
1150            trace!("successfully deserialized hardhat artifact");
1151            Ok(artifact.into_contract_bytecode())
1152        }
1153    }
1154
1155    fn contract_to_artifact(
1156        &self,
1157        file: &Path,
1158        name: &str,
1159        contract: Contract,
1160        source_file: Option<&SourceFile>,
1161    ) -> Self::Artifact {
1162        MinimalCombinedArtifacts::default().contract_to_artifact(file, name, contract, source_file)
1163    }
1164
1165    fn standalone_source_file_to_artifact(
1166        &self,
1167        path: &Path,
1168        file: &VersionedSourceFile,
1169    ) -> Option<Self::Artifact> {
1170        MinimalCombinedArtifacts::default().standalone_source_file_to_artifact(path, file)
1171    }
1172}
1173
1174#[cfg(test)]
1175mod tests {
1176    use super::*;
1177
1178    #[test]
1179    fn is_artifact() {
1180        fn assert_artifact<T: Artifact>() {}
1181
1182        assert_artifact::<CompactContractBytecode>();
1183        assert_artifact::<serde_json::Value>();
1184    }
1185
1186    #[test]
1187    fn can_find_alternate_paths() {
1188        let mut already_taken = HashSet::new();
1189
1190        let file = Path::new("v1/tokens/Greeter.sol");
1191        let conflict = PathBuf::from("out/Greeter.sol/Greeter.json");
1192        let artifacts_folder = Path::new("out");
1193
1194        let alternative = ConfigurableArtifacts::conflict_free_output_file(
1195            &already_taken,
1196            conflict.clone(),
1197            file,
1198            artifacts_folder,
1199        );
1200        assert_eq!(alternative.to_slash_lossy(), "out/tokens/Greeter.sol/Greeter.json");
1201
1202        already_taken.insert("out/tokens/Greeter.sol/Greeter.json".to_lowercase());
1203        let alternative = ConfigurableArtifacts::conflict_free_output_file(
1204            &already_taken,
1205            conflict.clone(),
1206            file,
1207            artifacts_folder,
1208        );
1209        assert_eq!(alternative.to_slash_lossy(), "out/v1/tokens/Greeter.sol/Greeter.json");
1210
1211        already_taken.insert("out/v1/tokens/Greeter.sol/Greeter.json".to_lowercase());
1212        let alternative = ConfigurableArtifacts::conflict_free_output_file(
1213            &already_taken,
1214            conflict,
1215            file,
1216            artifacts_folder,
1217        );
1218        assert_eq!(alternative, PathBuf::from("Greeter.sol_1/Greeter.json"));
1219    }
1220
1221    #[test]
1222    fn can_find_alternate_path_conflict() {
1223        let mut already_taken = HashSet::new();
1224
1225        let file = "/Users/carter/dev/goldfinch/mono/packages/protocol/test/forge/mainnet/utils/BaseMainnetForkingTest.t.sol";
1226        let conflict = PathBuf::from("/Users/carter/dev/goldfinch/mono/packages/protocol/artifacts/BaseMainnetForkingTest.t.sol/BaseMainnetForkingTest.json");
1227        already_taken.insert("/Users/carter/dev/goldfinch/mono/packages/protocol/artifacts/BaseMainnetForkingTest.t.sol/BaseMainnetForkingTest.json".into());
1228
1229        let alternative = ConfigurableArtifacts::conflict_free_output_file(
1230            &already_taken,
1231            conflict,
1232            file.as_ref(),
1233            "/Users/carter/dev/goldfinch/mono/packages/protocol/artifacts".as_ref(),
1234        );
1235
1236        assert_eq!(alternative.to_slash_lossy(), "/Users/carter/dev/goldfinch/mono/packages/protocol/artifacts/utils/BaseMainnetForkingTest.t.sol/BaseMainnetForkingTest.json");
1237    }
1238
1239    fn assert_artifact<T: crate::Artifact>() {}
1240
1241    #[test]
1242    fn test() {
1243        assert_artifact::<CompactContractBytecode>();
1244        assert_artifact::<CompactContractBytecodeCow<'static>>();
1245    }
1246}