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