1use 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#[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 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 pub fn with_slashed_paths(mut self) -> Self {
70 self.slash_paths();
71 self
72 }
73
74 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 pub fn with_stripped_file_prefixes(mut self, base: &Path) -> Self {
83 self.strip_file_prefixes(base);
84 self
85 }
86
87 pub fn slug(&self) -> String {
92 format!("{}.json:{}", self.path.file_stem().unwrap().to_string_lossy(), self.name)
93 }
94
95 pub fn identifier(&self) -> String {
97 format!("{}:{}", self.source.display(), self.name)
98 }
99
100 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#[derive(Clone, Debug, PartialEq, Eq)]
115pub struct ArtifactFile<T> {
116 pub artifact: T,
118 pub file: PathBuf,
120 pub version: Version,
122 pub build_id: String,
123 pub profile: String,
124}
125
126impl<T: Serialize> ArtifactFile<T> {
127 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 pub fn join(&mut self, root: &Path) {
138 self.file = root.join(&self.file);
139 }
140
141 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
149pub(crate) type ArtifactsMap<T> = FileToContractsMap<Vec<ArtifactFile<T>>>;
151
152#[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 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 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 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 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 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 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 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 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 pub fn has_artifact(&self, artifact_path: &Path) -> bool {
288 self.artifact_files().any(|artifact| artifact.file == artifact_path)
289 }
290
291 pub fn artifact_files(&self) -> impl Iterator<Item = &ArtifactFile<T>> {
293 self.0.values().flat_map(BTreeMap::values).flatten()
294 }
295
296 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 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 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 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 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 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 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 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 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 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
446pub trait Artifact {
448 fn into_inner(self) -> (Option<JsonAbi>, Option<Bytes>);
450
451 fn into_compact_contract(self) -> CompactContract;
453
454 fn into_contract_bytecode(self) -> CompactContractBytecode;
456
457 fn into_parts(self) -> (Option<JsonAbi>, Option<Bytes>, Option<Bytes>);
459
460 fn into_abi(self) -> Option<JsonAbi>
462 where
463 Self: Sized,
464 {
465 self.into_parts().0
466 }
467
468 fn into_bytecode_bytes(self) -> Option<Bytes>
470 where
471 Self: Sized,
472 {
473 self.into_parts().1
474 }
475 fn into_deployed_bytecode_bytes(self) -> Option<Bytes>
477 where
478 Self: Sized,
479 {
480 self.into_parts().2
481 }
482
483 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 fn get_contract_bytecode(&self) -> CompactContractBytecodeCow<'_>;
500
501 fn get_bytecode(&self) -> Option<Cow<'_, CompactBytecode>> {
503 self.get_contract_bytecode().bytecode
504 }
505
506 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 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 fn get_deployed_bytecode(&self) -> Option<Cow<'_, CompactDeployedBytecode>> {
526 self.get_contract_bytecode().deployed_bytecode
527 }
528
529 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 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 fn get_abi(&self) -> Option<Cow<'_, JsonAbi>> {
549 self.get_contract_bytecode().abi
550 }
551
552 fn get_source_map(&self) -> Option<std::result::Result<SourceMap, SyntaxError>> {
557 self.get_bytecode()?.source_map()
558 }
559
560 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 fn get_source_map_deployed(&self) -> Option<std::result::Result<SourceMap, SyntaxError>> {
573 self.get_deployed_bytecode()?.source_map()
574 }
575
576 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
612pub trait ArtifactOutput {
625 type Artifact: Artifact + DeserializeOwned + Serialize + fmt::Debug + Send + Sync;
627 type CompilerContract: CompilerContract;
628
629 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 fn handle_artifacts(
656 &self,
657 _contracts: &VersionedContracts<Self::CompilerContract>,
658 _artifacts: &Artifacts<Self::Artifact>,
659 ) -> Result<()> {
660 Ok(())
661 }
662
663 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 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)] 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 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 trace!("no conflict free output file found after traversing the file");
725
726 let mut num = 1;
727
728 loop {
729 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 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 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 fn read_cached_artifact(path: &Path) -> Result<Self::Artifact> {
790 utils::read_json_file(path)
791 }
792
793 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 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 #[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 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 Self::conflict_free_output_file(already_taken, path, file, artifacts_folder)
852 } else {
853 path
854 }
855 }
856 }
857
858 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 let mut non_standalone_sources = HashSet::new();
874
875 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 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 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 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 if source.source_file.ast.is_none()
965 || source.source_file.contains_contract_definition()
966 {
967 continue;
968 }
969
970 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 fn standalone_source_file_to_artifact(
1021 &self,
1022 _path: &Path,
1023 _file: &VersionedSourceFile,
1024 ) -> Option<Self::Artifact>;
1025
1026 fn is_dirty(&self, _artifact_file: &ArtifactFile<Self::Artifact>) -> Result<bool> {
1028 Ok(false)
1029 }
1030
1031 fn handle_cached_artifacts(&self, _artifacts: &Artifacts<Self::Artifact>) -> Result<()> {
1033 Ok(())
1034 }
1035}
1036
1037#[derive(Clone, Debug, Default)]
1039#[non_exhaustive]
1040pub struct OutputContext<'a> {
1041 pub existing_artifacts: BTreeMap<&'a Path, &'a CachedArtifacts>,
1053}
1054
1055impl<'a> OutputContext<'a> {
1058 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 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#[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#[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}