1use alloy_json_abi::JsonAbi;
4use alloy_primitives::Bytes;
5use foundry_compilers_artifacts::{
6 BytecodeObject, CompactBytecode, CompactContract, CompactContractBytecode,
7 CompactContractBytecodeCow, CompactDeployedBytecode, Contract, FileToContractsMap, SourceFile,
8 hh::HardhatArtifact,
9 sourcemap::{SourceMap, SyntaxError},
10};
11use foundry_compilers_core::{
12 error::{Result, SolcError, SolcIoError},
13 utils::{self, strip_prefix_owned},
14};
15use path_slash::PathBufExt;
16use semver::Version;
17use serde::{Deserialize, Serialize, de::DeserializeOwned};
18use std::{
19 borrow::Cow,
20 collections::{HashMap, HashSet, btree_map::BTreeMap},
21 ffi::OsString,
22 fmt, fs,
23 hash::Hash,
24 ops::Deref,
25 path::{Path, PathBuf},
26};
27
28mod configurable;
29pub use configurable::*;
30
31mod hh;
32pub use hh::*;
33
34use crate::{
35 CompilerContract, ProjectPathsConfig,
36 cache::{CachedArtifacts, CompilerCache},
37 output::{
38 contracts::VersionedContracts,
39 sources::{VersionedSourceFile, VersionedSourceFiles},
40 },
41};
42
43#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
45pub struct ArtifactId {
46 pub path: PathBuf,
48 pub name: String,
49 pub source: PathBuf,
51 pub version: Version,
53 pub build_id: String,
55 pub profile: String,
56}
57
58impl ArtifactId {
59 #[allow(clippy::missing_const_for_fn)]
61 pub fn slash_paths(&mut self) {
62 #[cfg(windows)]
63 {
64 self.path = self.path.to_slash_lossy().as_ref().into();
65 self.source = self.source.to_slash_lossy().as_ref().into();
66 }
67 }
68
69 #[allow(clippy::missing_const_for_fn)]
71 pub fn with_slashed_paths(mut self) -> Self {
72 self.slash_paths();
73 self
74 }
75
76 pub fn strip_file_prefixes(&mut self, base: &Path) {
78 if let Ok(stripped) = self.source.strip_prefix(base) {
79 self.source = stripped.to_path_buf();
80 }
81 }
82
83 pub fn with_stripped_file_prefixes(mut self, base: &Path) -> Self {
85 self.strip_file_prefixes(base);
86 self
87 }
88
89 pub fn slug(&self) -> String {
94 format!("{}.json:{}", self.path.file_stem().unwrap().to_string_lossy(), self.name)
95 }
96
97 pub fn identifier(&self) -> String {
99 format!("{}:{}", self.source.display(), self.name)
100 }
101
102 pub fn slug_versioned(&self) -> String {
104 format!(
105 "{}.{}.{}.{}.json:{}",
106 self.path.file_stem().unwrap().to_string_lossy(),
107 self.version.major,
108 self.version.minor,
109 self.version.patch,
110 self.name
111 )
112 }
113}
114
115#[derive(Clone, Debug, PartialEq, Eq)]
117pub struct ArtifactFile<T> {
118 pub artifact: T,
120 pub file: PathBuf,
122 pub version: Version,
124 pub build_id: String,
125 pub profile: String,
126}
127
128impl<T: Serialize> ArtifactFile<T> {
129 pub fn write(&self) -> Result<()> {
131 trace!("writing artifact file {:?} {}", self.file, self.version);
132 utils::create_parent_dir_all(&self.file)?;
133 utils::write_json_file(&self.artifact, &self.file, 64 * 1024)
134 }
135}
136
137impl<T> ArtifactFile<T> {
138 pub fn join(&mut self, root: &Path) {
140 self.file = root.join(&self.file);
141 }
142
143 pub fn strip_prefix(&mut self, base: &Path) {
145 if let Ok(stripped) = self.file.strip_prefix(base) {
146 self.file = stripped.to_path_buf();
147 }
148 }
149}
150
151pub(crate) type ArtifactsMap<T> = FileToContractsMap<Vec<ArtifactFile<T>>>;
153
154#[derive(Clone, Debug, PartialEq, Eq)]
156pub struct Artifacts<T>(pub ArtifactsMap<T>);
157
158impl<T> From<ArtifactsMap<T>> for Artifacts<T> {
159 fn from(m: ArtifactsMap<T>) -> Self {
160 Self(m)
161 }
162}
163
164impl<'a, T> IntoIterator for &'a Artifacts<T> {
165 type Item = (&'a PathBuf, &'a BTreeMap<String, Vec<ArtifactFile<T>>>);
166 type IntoIter =
167 std::collections::btree_map::Iter<'a, PathBuf, BTreeMap<String, Vec<ArtifactFile<T>>>>;
168
169 fn into_iter(self) -> Self::IntoIter {
170 self.0.iter()
171 }
172}
173
174impl<T> IntoIterator for Artifacts<T> {
175 type Item = (PathBuf, BTreeMap<String, Vec<ArtifactFile<T>>>);
176 type IntoIter =
177 std::collections::btree_map::IntoIter<PathBuf, BTreeMap<String, Vec<ArtifactFile<T>>>>;
178
179 fn into_iter(self) -> Self::IntoIter {
180 self.0.into_iter()
181 }
182}
183
184impl<T> Default for Artifacts<T> {
185 fn default() -> Self {
186 Self(Default::default())
187 }
188}
189
190impl<T> AsRef<ArtifactsMap<T>> for Artifacts<T> {
191 fn as_ref(&self) -> &ArtifactsMap<T> {
192 &self.0
193 }
194}
195
196impl<T> AsMut<ArtifactsMap<T>> for Artifacts<T> {
197 fn as_mut(&mut self) -> &mut ArtifactsMap<T> {
198 &mut self.0
199 }
200}
201
202impl<T> Deref for Artifacts<T> {
203 type Target = ArtifactsMap<T>;
204
205 fn deref(&self) -> &Self::Target {
206 &self.0
207 }
208}
209
210impl<T: Serialize> Artifacts<T> {
211 pub fn write_all(&self) -> Result<()> {
213 for artifact in self.artifact_files() {
214 artifact.write()?;
215 }
216 Ok(())
217 }
218}
219
220impl<T> Artifacts<T> {
221 #[allow(clippy::missing_const_for_fn)]
223 pub fn slash_paths(&mut self) {
224 #[cfg(windows)]
225 {
226 self.0 = std::mem::take(&mut self.0)
227 .into_iter()
228 .map(|(path, files)| (PathBuf::from(path.to_slash_lossy().as_ref()), files))
229 .collect()
230 }
231 }
232
233 pub fn into_inner(self) -> ArtifactsMap<T> {
234 self.0
235 }
236
237 pub fn join_all(&mut self, root: &Path) -> &mut Self {
239 self.artifact_files_mut().for_each(|artifact| artifact.join(root));
240 self
241 }
242
243 pub fn strip_prefix_all(&mut self, base: &Path) -> &mut Self {
245 self.artifact_files_mut().for_each(|artifact| artifact.strip_prefix(base));
246 self
247 }
248
249 fn get_contract_artifact_files(&self, contract_name: &str) -> Option<&Vec<ArtifactFile<T>>> {
251 self.0.values().find_map(|all| all.get(contract_name))
252 }
253
254 pub fn find_artifact(
256 &self,
257 file: &Path,
258 contract_name: &str,
259 version: &Version,
260 ) -> Option<&ArtifactFile<T>> {
261 self.0
262 .get(file)
263 .and_then(|contracts| contracts.get(contract_name))
264 .and_then(|artifacts| artifacts.iter().find(|artifact| artifact.version == *version))
265 }
266
267 pub fn find_artifact_with_profile(
269 &self,
270 file: &Path,
271 contract_name: &str,
272 version: &Version,
273 profile: &str,
274 ) -> Option<&ArtifactFile<T>> {
275 self.0.get(file).and_then(|contracts| contracts.get(contract_name)).and_then(|artifacts| {
276 artifacts
277 .iter()
278 .find(|artifact| artifact.version == *version && artifact.profile == profile)
279 })
280 }
281
282 pub fn has_contract_artifact(&self, contract_name: &str, artifact_path: &Path) -> bool {
284 self.get_contract_artifact_files(contract_name)
285 .map(|artifacts| artifacts.iter().any(|artifact| artifact.file == artifact_path))
286 .unwrap_or_default()
287 }
288
289 pub fn has_artifact(&self, artifact_path: &Path) -> bool {
291 self.artifact_files().any(|artifact| artifact.file == artifact_path)
292 }
293
294 pub fn artifact_files(&self) -> impl Iterator<Item = &ArtifactFile<T>> {
296 self.0.values().flat_map(BTreeMap::values).flatten()
297 }
298
299 pub fn artifact_files_mut(&mut self) -> impl Iterator<Item = &mut ArtifactFile<T>> {
301 self.0.values_mut().flat_map(BTreeMap::values_mut).flatten()
302 }
303
304 pub fn artifacts<O: ArtifactOutput<Artifact = T>>(
308 &self,
309 ) -> impl Iterator<Item = (ArtifactId, &T)> + '_ {
310 self.0.iter().flat_map(|(source, contract_artifacts)| {
311 contract_artifacts.values().flat_map(move |artifacts| {
312 artifacts.iter().filter_map(move |artifact| {
313 O::contract_name(&artifact.file).map(|name| {
314 (
315 ArtifactId {
316 path: PathBuf::from(&artifact.file),
317 name,
318 source: source.clone(),
319 version: artifact.version.clone(),
320 build_id: artifact.build_id.clone(),
321 profile: artifact.profile.clone(),
322 }
323 .with_slashed_paths(),
324 &artifact.artifact,
325 )
326 })
327 })
328 })
329 })
330 }
331
332 pub fn into_artifacts<O: ArtifactOutput<Artifact = T>>(
334 self,
335 ) -> impl Iterator<Item = (ArtifactId, T)> {
336 self.0.into_iter().flat_map(|(source, contract_artifacts)| {
337 contract_artifacts.into_values().flat_map(move |artifacts| {
338 let source = source.clone();
339 artifacts.into_iter().filter_map(move |artifact| {
340 O::contract_name(&artifact.file).map(|name| {
341 (
342 ArtifactId {
343 path: PathBuf::from(&artifact.file),
344 name,
345 source: source.clone(),
346 version: artifact.version,
347 build_id: artifact.build_id.clone(),
348 profile: artifact.profile.clone(),
349 }
350 .with_slashed_paths(),
351 artifact.artifact,
352 )
353 })
354 })
355 })
356 })
357 }
358
359 pub fn artifacts_with_files(&self) -> impl Iterator<Item = (&PathBuf, &String, &T)> + '_ {
365 self.0.iter().flat_map(|(f, contract_artifacts)| {
366 contract_artifacts.iter().flat_map(move |(name, artifacts)| {
367 artifacts.iter().map(move |artifact| (f, name, &artifact.artifact))
368 })
369 })
370 }
371
372 pub fn into_artifacts_with_files(self) -> impl Iterator<Item = (PathBuf, String, T)> {
376 self.0.into_iter().flat_map(|(f, contract_artifacts)| {
377 contract_artifacts.into_iter().flat_map(move |(name, artifacts)| {
378 let contract_name = name;
379 let file = f.clone();
380 artifacts
381 .into_iter()
382 .map(move |artifact| (file.clone(), contract_name.clone(), artifact.artifact))
383 })
384 })
385 }
386
387 pub fn into_stripped_file_prefixes(self, base: &Path) -> Self {
390 let artifacts =
391 self.0.into_iter().map(|(path, c)| (strip_prefix_owned(path, base), c)).collect();
392 Self(artifacts)
393 }
394
395 pub fn find_first(&self, contract_name: &str) -> Option<&T> {
397 self.0.iter().find_map(|(_file, contracts)| {
398 contracts.get(contract_name).and_then(|c| c.first().map(|a| &a.artifact))
399 })
400 }
401
402 pub fn find(&self, contract_path: &Path, contract_name: &str) -> Option<&T> {
404 self.0.iter().filter(|(path, _)| path.as_path() == contract_path).find_map(
405 |(_file, contracts)| {
406 contracts.get(contract_name).and_then(|c| c.first().map(|a| &a.artifact))
407 },
408 )
409 }
410
411 pub fn remove(&mut self, contract_path: &Path, contract_name: &str) -> Option<T> {
413 self.0.iter_mut().filter(|(path, _)| path.as_path() == contract_path).find_map(
414 |(_file, contracts)| {
415 let mut artifact = None;
416 if let Some((c, mut artifacts)) = contracts.remove_entry(contract_name) {
417 if !artifacts.is_empty() {
418 artifact = Some(artifacts.remove(0).artifact);
419 }
420 if !artifacts.is_empty() {
421 contracts.insert(c, artifacts);
422 }
423 }
424 artifact
425 },
426 )
427 }
428
429 pub fn remove_first(&mut self, contract_name: &str) -> Option<T> {
434 self.0.iter_mut().find_map(|(_file, contracts)| {
435 let mut artifact = None;
436 if let Some((c, mut artifacts)) = contracts.remove_entry(contract_name) {
437 if !artifacts.is_empty() {
438 artifact = Some(artifacts.remove(0).artifact);
439 }
440 if !artifacts.is_empty() {
441 contracts.insert(c, artifacts);
442 }
443 }
444 artifact
445 })
446 }
447}
448
449pub trait Artifact {
451 fn into_inner(self) -> (Option<JsonAbi>, Option<Bytes>);
453
454 fn into_compact_contract(self) -> CompactContract;
456
457 fn into_contract_bytecode(self) -> CompactContractBytecode;
459
460 fn into_parts(self) -> (Option<JsonAbi>, Option<Bytes>, Option<Bytes>);
462
463 fn into_abi(self) -> Option<JsonAbi>
465 where
466 Self: Sized,
467 {
468 self.into_parts().0
469 }
470
471 fn into_bytecode_bytes(self) -> Option<Bytes>
473 where
474 Self: Sized,
475 {
476 self.into_parts().1
477 }
478 fn into_deployed_bytecode_bytes(self) -> Option<Bytes>
480 where
481 Self: Sized,
482 {
483 self.into_parts().2
484 }
485
486 fn try_into_parts(self) -> Result<(JsonAbi, Bytes, Bytes)>
488 where
489 Self: Sized,
490 {
491 let (abi, bytecode, deployed_bytecode) = self.into_parts();
492
493 Ok((
494 abi.ok_or_else(|| SolcError::msg("abi missing"))?,
495 bytecode.ok_or_else(|| SolcError::msg("bytecode missing"))?,
496 deployed_bytecode.ok_or_else(|| SolcError::msg("deployed bytecode missing"))?,
497 ))
498 }
499
500 fn get_contract_bytecode(&self) -> CompactContractBytecodeCow<'_>;
503
504 fn get_bytecode(&self) -> Option<Cow<'_, CompactBytecode>> {
506 self.get_contract_bytecode().bytecode
507 }
508
509 fn get_bytecode_object(&self) -> Option<Cow<'_, BytecodeObject>> {
511 let val = match self.get_bytecode()? {
512 Cow::Borrowed(b) => Cow::Borrowed(&b.object),
513 Cow::Owned(b) => Cow::Owned(b.object),
514 };
515 Some(val)
516 }
517
518 fn get_bytecode_bytes(&self) -> Option<Cow<'_, Bytes>> {
520 let val = match self.get_bytecode_object()? {
521 Cow::Borrowed(b) => Cow::Borrowed(b.as_bytes()?),
522 Cow::Owned(b) => Cow::Owned(b.into_bytes()?),
523 };
524 Some(val)
525 }
526
527 fn get_deployed_bytecode(&self) -> Option<Cow<'_, CompactDeployedBytecode>> {
529 self.get_contract_bytecode().deployed_bytecode
530 }
531
532 fn get_deployed_bytecode_object(&self) -> Option<Cow<'_, BytecodeObject>> {
534 let val = match self.get_deployed_bytecode()? {
535 Cow::Borrowed(b) => Cow::Borrowed(&b.bytecode.as_ref()?.object),
536 Cow::Owned(b) => Cow::Owned(b.bytecode?.object),
537 };
538 Some(val)
539 }
540
541 fn get_deployed_bytecode_bytes(&self) -> Option<Cow<'_, Bytes>> {
543 let val = match self.get_deployed_bytecode_object()? {
544 Cow::Borrowed(b) => Cow::Borrowed(b.as_bytes()?),
545 Cow::Owned(b) => Cow::Owned(b.into_bytes()?),
546 };
547 Some(val)
548 }
549
550 fn get_abi(&self) -> Option<Cow<'_, JsonAbi>> {
552 self.get_contract_bytecode().abi
553 }
554
555 fn get_source_map(&self) -> Option<std::result::Result<SourceMap, SyntaxError>> {
560 self.get_bytecode()?.source_map()
561 }
562
563 fn get_source_map_str(&self) -> Option<Cow<'_, str>> {
565 match self.get_bytecode()? {
566 Cow::Borrowed(code) => code.source_map.as_deref().map(Cow::Borrowed),
567 Cow::Owned(code) => code.source_map.map(Cow::Owned),
568 }
569 }
570
571 fn get_source_map_deployed(&self) -> Option<std::result::Result<SourceMap, SyntaxError>> {
576 self.get_deployed_bytecode()?.source_map()
577 }
578
579 fn get_source_map_deployed_str(&self) -> Option<Cow<'_, str>> {
581 match self.get_deployed_bytecode()? {
582 Cow::Borrowed(code) => code.bytecode.as_ref()?.source_map.as_deref().map(Cow::Borrowed),
583 Cow::Owned(code) => code.bytecode?.source_map.map(Cow::Owned),
584 }
585 }
586}
587
588impl<T> Artifact for T
589where
590 T: Into<CompactContractBytecode> + Into<CompactContract>,
591 for<'a> &'a T: Into<CompactContractBytecodeCow<'a>>,
592{
593 fn into_inner(self) -> (Option<JsonAbi>, Option<Bytes>) {
594 let artifact = self.into_compact_contract();
595 (artifact.abi, artifact.bin.and_then(|bin| bin.into_bytes()))
596 }
597
598 fn into_compact_contract(self) -> CompactContract {
599 self.into()
600 }
601
602 fn into_contract_bytecode(self) -> CompactContractBytecode {
603 self.into()
604 }
605
606 fn into_parts(self) -> (Option<JsonAbi>, Option<Bytes>, Option<Bytes>) {
607 self.into_compact_contract().into_parts()
608 }
609
610 fn get_contract_bytecode(&self) -> CompactContractBytecodeCow<'_> {
611 self.into()
612 }
613}
614
615pub trait ArtifactOutput {
628 type Artifact: Artifact + DeserializeOwned + Serialize + fmt::Debug + Send + Sync;
630 type CompilerContract: CompilerContract;
631
632 fn on_output<L>(
637 &self,
638 contracts: &VersionedContracts<Self::CompilerContract>,
639 sources: &VersionedSourceFiles,
640 layout: &ProjectPathsConfig<L>,
641 ctx: OutputContext<'_>,
642 primary_profiles: &HashMap<PathBuf, &str>,
643 ) -> Result<Artifacts<Self::Artifact>> {
644 let mut artifacts =
645 self.output_to_artifacts(contracts, sources, ctx, layout, primary_profiles);
646 fs::create_dir_all(&layout.artifacts)
647 .map_err(|err| SolcIoError::new(err, &layout.artifacts))?;
648
649 artifacts.join_all(&layout.artifacts);
650 artifacts.write_all()?;
651
652 self.handle_artifacts(contracts, &artifacts)?;
653
654 Ok(artifacts)
655 }
656
657 fn handle_artifacts(
659 &self,
660 _contracts: &VersionedContracts<Self::CompilerContract>,
661 _artifacts: &Artifacts<Self::Artifact>,
662 ) -> Result<()> {
663 Ok(())
664 }
665
666 fn output_file_name(
669 name: &str,
670 version: &Version,
671 profile: &str,
672 with_version: bool,
673 with_profile: bool,
674 ) -> PathBuf {
675 let mut name = name.to_string();
676 if with_version {
677 name.push_str(&format!(".{}.{}.{}", version.major, version.minor, version.patch));
678 }
679 if with_profile {
680 name.push_str(&format!(".{profile}"));
681 }
682 name.push_str(".json");
683 name.into()
684 }
685
686 fn conflict_free_output_file(
699 already_taken: &HashSet<String>,
700 conflict: PathBuf,
701 contract_file: &Path,
702 artifacts_folder: &Path,
703 ) -> PathBuf {
704 let mut rel_candidate = conflict;
705 if let Ok(stripped) = rel_candidate.strip_prefix(artifacts_folder) {
706 rel_candidate = stripped.to_path_buf();
707 }
708 #[allow(clippy::redundant_clone)] let mut candidate = rel_candidate.clone();
710 let mut current_parent = contract_file.parent();
711
712 while let Some(parent_name) = current_parent.and_then(|f| f.file_name()) {
713 candidate = Path::new(parent_name).join(&candidate);
715 let out_path = artifacts_folder.join(&candidate);
716 if !already_taken.contains(&out_path.to_slash_lossy().to_lowercase()) {
717 trace!("found alternative output file={:?} for {:?}", out_path, contract_file);
718 return out_path;
719 }
720 current_parent = current_parent.and_then(|f| f.parent());
721 }
722
723 trace!("no conflict free output file found after traversing the file");
728
729 let mut num = 1;
730
731 loop {
732 let mut components = rel_candidate.components();
735 let first = components.next().expect("path not empty");
736 let name = first.as_os_str();
737 let mut numerated = OsString::with_capacity(name.len() + 2);
738 numerated.push(name);
739 numerated.push("_");
740 numerated.push(num.to_string());
741
742 let candidate: PathBuf = Some(numerated.as_os_str())
743 .into_iter()
744 .chain(components.map(|c| c.as_os_str()))
745 .collect();
746 if !already_taken.contains(&candidate.to_slash_lossy().to_lowercase()) {
747 trace!("found alternative output file={:?} for {:?}", candidate, contract_file);
748 return candidate;
749 }
750
751 num += 1;
752 }
753 }
754
755 fn output_file(
759 contract_file: &Path,
760 name: &str,
761 version: &Version,
762 profile: &str,
763 with_version: bool,
764 with_profile: bool,
765 ) -> PathBuf {
766 contract_file
767 .file_name()
768 .map(Path::new)
769 .map(|p| {
770 p.join(Self::output_file_name(name, version, profile, with_version, with_profile))
771 })
772 .unwrap_or_else(|| {
773 Self::output_file_name(name, version, profile, with_version, with_profile)
774 })
775 }
776
777 fn contract_name(file: &Path) -> Option<String> {
782 file.file_stem().and_then(|s| s.to_str().map(|s| s.to_string()))
783 }
784
785 fn read_cached_artifact(path: &Path) -> Result<Self::Artifact> {
793 utils::read_json_file(path)
794 }
795
796 fn read_cached_artifacts<T, I>(files: I) -> Result<BTreeMap<PathBuf, Self::Artifact>>
800 where
801 I: IntoIterator<Item = T>,
802 T: Into<PathBuf>,
803 {
804 let mut artifacts = BTreeMap::default();
805 for path in files {
806 let path = path.into();
807 let artifact = Self::read_cached_artifact(&path)?;
808 artifacts.insert(path, artifact);
809 }
810 Ok(artifacts)
811 }
812
813 fn contract_to_artifact(
819 &self,
820 _file: &Path,
821 _name: &str,
822 contract: Self::CompilerContract,
823 source_file: Option<&SourceFile>,
824 ) -> Self::Artifact;
825
826 #[allow(clippy::too_many_arguments)]
829 fn get_artifact_path(
830 ctx: &OutputContext<'_>,
831 already_taken: &HashSet<String>,
832 file: &Path,
833 name: &str,
834 artifacts_folder: &Path,
835 version: &Version,
836 profile: &str,
837 with_version: bool,
838 with_profile: bool,
839 ) -> PathBuf {
840 if let Some(existing_artifact) = ctx.existing_artifact(file, name, version, profile) {
845 trace!("use existing artifact file {:?}", existing_artifact,);
846 existing_artifact.to_path_buf()
847 } else {
848 let path = Self::output_file(file, name, version, profile, with_version, with_profile);
849
850 let path = artifacts_folder.join(path);
851
852 if already_taken.contains(&path.to_slash_lossy().to_lowercase()) {
853 Self::conflict_free_output_file(already_taken, path, file, artifacts_folder)
855 } else {
856 path
857 }
858 }
859 }
860
861 fn output_to_artifacts<C>(
866 &self,
867 contracts: &VersionedContracts<Self::CompilerContract>,
868 sources: &VersionedSourceFiles,
869 ctx: OutputContext<'_>,
870 layout: &ProjectPathsConfig<C>,
871 primary_profiles: &HashMap<PathBuf, &str>,
872 ) -> Artifacts<Self::Artifact> {
873 let mut artifacts = ArtifactsMap::new();
874
875 let mut non_standalone_sources = HashSet::new();
877
878 let mut taken_paths_lowercase = ctx
880 .existing_artifacts
881 .values()
882 .flat_map(|artifacts| artifacts.values())
883 .flat_map(|artifacts| artifacts.values())
884 .flat_map(|artifacts| artifacts.values())
885 .map(|a| a.path.to_slash_lossy().to_lowercase())
886 .collect::<HashSet<_>>();
887
888 let mut files = contracts.keys().collect::<Vec<_>>();
889 files.sort_by(|&file1, &file2| {
891 (file1.components().count(), file1).cmp(&(file2.components().count(), file2))
892 });
893 for file in files {
894 for (name, versioned_contracts) in &contracts[file] {
895 let unique_versions =
896 versioned_contracts.iter().map(|c| &c.version).collect::<HashSet<_>>();
897 let unique_profiles =
898 versioned_contracts.iter().map(|c| &c.profile).collect::<HashSet<_>>();
899 let primary_profile = primary_profiles.get(file);
900
901 for contract in versioned_contracts {
902 non_standalone_sources.insert(file);
903
904 let source_file = sources.find_file_and_version(file, &contract.version);
906
907 let artifact_path = Self::get_artifact_path(
908 &ctx,
909 &taken_paths_lowercase,
910 file,
911 name,
912 layout.artifacts.as_path(),
913 &contract.version,
914 &contract.profile,
915 unique_versions.len() > 1,
916 unique_profiles.len() > 1
917 && primary_profile.is_none_or(|p| p != &contract.profile),
918 );
919
920 taken_paths_lowercase.insert(artifact_path.to_slash_lossy().to_lowercase());
921
922 trace!(
923 "use artifact file {:?} for contract file {} {}",
924 artifact_path,
925 file.display(),
926 contract.version
927 );
928
929 let artifact = self.contract_to_artifact(
930 file,
931 name,
932 contract.contract.clone(),
933 source_file,
934 );
935
936 let artifact = ArtifactFile {
937 artifact,
938 file: artifact_path,
939 version: contract.version.clone(),
940 build_id: contract.build_id.clone(),
941 profile: contract.profile.clone(),
942 };
943
944 artifacts
945 .entry(file.clone())
946 .or_default()
947 .entry(name.clone())
948 .or_default()
949 .push(artifact);
950 }
951 }
952 }
953
954 for (file, sources) in sources.as_ref() {
959 let unique_versions = sources.iter().map(|s| &s.version).collect::<HashSet<_>>();
960 let unique_profiles = sources.iter().map(|s| &s.profile).collect::<HashSet<_>>();
961 for source in sources {
962 if !non_standalone_sources.contains(file) {
963 if source.source_file.ast.is_none()
968 || source.source_file.contains_contract_definition()
969 {
970 continue;
971 }
972
973 if let Some(name) = Path::new(file).file_stem().and_then(|stem| stem.to_str())
975 && let Some(artifact) =
976 self.standalone_source_file_to_artifact(file, source)
977 {
978 let artifact_path = Self::get_artifact_path(
979 &ctx,
980 &taken_paths_lowercase,
981 file,
982 name,
983 &layout.artifacts,
984 &source.version,
985 &source.profile,
986 unique_versions.len() > 1,
987 unique_profiles.len() > 1,
988 );
989
990 taken_paths_lowercase.insert(artifact_path.to_slash_lossy().to_lowercase());
991
992 artifacts
993 .entry(file.clone())
994 .or_default()
995 .entry(name.to_string())
996 .or_default()
997 .push(ArtifactFile {
998 artifact,
999 file: artifact_path,
1000 version: source.version.clone(),
1001 build_id: source.build_id.clone(),
1002 profile: source.profile.clone(),
1003 });
1004 }
1005 }
1006 }
1007 }
1008
1009 Artifacts(artifacts)
1010 }
1011
1012 fn standalone_source_file_to_artifact(
1022 &self,
1023 _path: &Path,
1024 _file: &VersionedSourceFile,
1025 ) -> Option<Self::Artifact>;
1026
1027 fn is_dirty(&self, _artifact_file: &ArtifactFile<Self::Artifact>) -> Result<bool> {
1029 Ok(false)
1030 }
1031
1032 fn handle_cached_artifacts(&self, _artifacts: &Artifacts<Self::Artifact>) -> Result<()> {
1034 Ok(())
1035 }
1036}
1037
1038#[derive(Clone, Debug, Default)]
1040#[non_exhaustive]
1041pub struct OutputContext<'a> {
1042 pub existing_artifacts: BTreeMap<&'a Path, &'a CachedArtifacts>,
1054}
1055
1056impl<'a> OutputContext<'a> {
1059 pub fn new<S>(cache: &'a CompilerCache<S>) -> Self {
1061 let existing_artifacts = cache
1062 .files
1063 .iter()
1064 .map(|(file, entry)| (file.as_path(), &entry.artifacts))
1065 .collect::<BTreeMap<_, _>>();
1066
1067 Self { existing_artifacts }
1068 }
1069
1070 pub fn existing_artifact(
1075 &self,
1076 file: &Path,
1077 contract: &str,
1078 version: &Version,
1079 profile: &str,
1080 ) -> Option<&Path> {
1081 self.existing_artifacts
1082 .get(file)
1083 .and_then(|contracts| contracts.get(contract))
1084 .and_then(|versions| versions.get(version))
1085 .and_then(|profiles| profiles.get(profile))
1086 .map(|a| a.path.as_path())
1087 }
1088}
1089
1090#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1101pub struct MinimalCombinedArtifacts {
1102 _priv: (),
1103}
1104
1105impl ArtifactOutput for MinimalCombinedArtifacts {
1106 type Artifact = CompactContractBytecode;
1107 type CompilerContract = Contract;
1108
1109 fn contract_to_artifact(
1110 &self,
1111 _file: &Path,
1112 _name: &str,
1113 contract: Contract,
1114 _source_file: Option<&SourceFile>,
1115 ) -> Self::Artifact {
1116 Self::Artifact::from(contract)
1117 }
1118
1119 fn standalone_source_file_to_artifact(
1120 &self,
1121 _path: &Path,
1122 _file: &VersionedSourceFile,
1123 ) -> Option<Self::Artifact> {
1124 None
1125 }
1126}
1127
1128#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1131pub struct MinimalCombinedArtifactsHardhatFallback {
1132 _priv: (),
1133}
1134
1135impl ArtifactOutput for MinimalCombinedArtifactsHardhatFallback {
1136 type Artifact = CompactContractBytecode;
1137 type CompilerContract = Contract;
1138
1139 fn on_output<C>(
1140 &self,
1141 output: &VersionedContracts<Contract>,
1142 sources: &VersionedSourceFiles,
1143 layout: &ProjectPathsConfig<C>,
1144 ctx: OutputContext<'_>,
1145 primary_profiles: &HashMap<PathBuf, &str>,
1146 ) -> Result<Artifacts<Self::Artifact>> {
1147 MinimalCombinedArtifacts::default().on_output(
1148 output,
1149 sources,
1150 layout,
1151 ctx,
1152 primary_profiles,
1153 )
1154 }
1155
1156 fn read_cached_artifact(path: &Path) -> Result<Self::Artifact> {
1157 #[derive(Deserialize)]
1158 #[serde(untagged)]
1159 enum Artifact {
1160 Compact(CompactContractBytecode),
1161 Hardhat(HardhatArtifact),
1162 }
1163
1164 Ok(match utils::read_json_file::<Artifact>(path)? {
1165 Artifact::Compact(c) => c,
1166 Artifact::Hardhat(h) => h.into_contract_bytecode(),
1167 })
1168 }
1169
1170 fn contract_to_artifact(
1171 &self,
1172 file: &Path,
1173 name: &str,
1174 contract: Contract,
1175 source_file: Option<&SourceFile>,
1176 ) -> Self::Artifact {
1177 MinimalCombinedArtifacts::default().contract_to_artifact(file, name, contract, source_file)
1178 }
1179
1180 fn standalone_source_file_to_artifact(
1181 &self,
1182 path: &Path,
1183 file: &VersionedSourceFile,
1184 ) -> Option<Self::Artifact> {
1185 MinimalCombinedArtifacts::default().standalone_source_file_to_artifact(path, file)
1186 }
1187}
1188
1189#[cfg(test)]
1190mod tests {
1191 use super::*;
1192
1193 #[test]
1194 fn is_artifact() {
1195 fn assert_artifact<T: Artifact>() {}
1196
1197 assert_artifact::<CompactContractBytecode>();
1198 assert_artifact::<serde_json::Value>();
1199 }
1200
1201 #[test]
1202 fn can_find_alternate_paths() {
1203 let mut already_taken = HashSet::new();
1204
1205 let file = Path::new("v1/tokens/Greeter.sol");
1206 let conflict = PathBuf::from("out/Greeter.sol/Greeter.json");
1207 let artifacts_folder = Path::new("out");
1208
1209 let alternative = ConfigurableArtifacts::conflict_free_output_file(
1210 &already_taken,
1211 conflict.clone(),
1212 file,
1213 artifacts_folder,
1214 );
1215 assert_eq!(alternative.to_slash_lossy(), "out/tokens/Greeter.sol/Greeter.json");
1216
1217 already_taken.insert("out/tokens/Greeter.sol/Greeter.json".to_lowercase());
1218 let alternative = ConfigurableArtifacts::conflict_free_output_file(
1219 &already_taken,
1220 conflict.clone(),
1221 file,
1222 artifacts_folder,
1223 );
1224 assert_eq!(alternative.to_slash_lossy(), "out/v1/tokens/Greeter.sol/Greeter.json");
1225
1226 already_taken.insert("out/v1/tokens/Greeter.sol/Greeter.json".to_lowercase());
1227 let alternative = ConfigurableArtifacts::conflict_free_output_file(
1228 &already_taken,
1229 conflict,
1230 file,
1231 artifacts_folder,
1232 );
1233 assert_eq!(alternative, PathBuf::from("Greeter.sol_1/Greeter.json"));
1234 }
1235
1236 #[test]
1237 fn can_find_alternate_path_conflict() {
1238 let mut already_taken = HashSet::new();
1239
1240 let file = "/Users/carter/dev/goldfinch/mono/packages/protocol/test/forge/mainnet/utils/BaseMainnetForkingTest.t.sol";
1241 let conflict = PathBuf::from(
1242 "/Users/carter/dev/goldfinch/mono/packages/protocol/artifacts/BaseMainnetForkingTest.t.sol/BaseMainnetForkingTest.json",
1243 );
1244 already_taken.insert("/Users/carter/dev/goldfinch/mono/packages/protocol/artifacts/BaseMainnetForkingTest.t.sol/BaseMainnetForkingTest.json".into());
1245
1246 let alternative = ConfigurableArtifacts::conflict_free_output_file(
1247 &already_taken,
1248 conflict,
1249 file.as_ref(),
1250 "/Users/carter/dev/goldfinch/mono/packages/protocol/artifacts".as_ref(),
1251 );
1252
1253 assert_eq!(
1254 alternative.to_slash_lossy(),
1255 "/Users/carter/dev/goldfinch/mono/packages/protocol/artifacts/utils/BaseMainnetForkingTest.t.sol/BaseMainnetForkingTest.json"
1256 );
1257 }
1258
1259 fn assert_artifact<T: crate::Artifact>() {}
1260
1261 #[test]
1262 fn test() {
1263 assert_artifact::<CompactContractBytecode>();
1264 assert_artifact::<CompactContractBytecodeCow<'static>>();
1265 }
1266
1267 #[test]
1268 fn deployed_source_map_string_uses_runtime_bytecode() {
1269 let artifact = CompactContractBytecode {
1270 abi: None,
1271 bytecode: Some(CompactBytecode {
1272 object: BytecodeObject::Bytecode(Default::default()),
1273 source_map: Some("creation-map".to_string()),
1274 link_references: BTreeMap::new(),
1275 }),
1276 deployed_bytecode: Some(CompactDeployedBytecode {
1277 bytecode: Some(CompactBytecode {
1278 object: BytecodeObject::Bytecode(Default::default()),
1279 source_map: Some("runtime-map".to_string()),
1280 link_references: BTreeMap::new(),
1281 }),
1282 immutable_references: BTreeMap::new(),
1283 }),
1284 };
1285
1286 assert_eq!(artifact.get_source_map_deployed_str().as_deref(), Some("runtime-map"));
1287 }
1288}