1pub mod git;
11pub(crate) mod ipfs;
12mod member;
13pub mod path;
14pub mod reg;
15
16use self::git::Url;
17use crate::manifest::GenericManifestFile;
18use crate::{
19 manifest::{self, MemberManifestFiles, PackageManifestFile},
20 pkg::{ManifestMap, PinnedId},
21};
22use anyhow::{anyhow, bail, Result};
23use serde::{Deserialize, Serialize};
24use std::{
25 collections::hash_map,
26 fmt,
27 hash::{Hash, Hasher},
28 path::{Path, PathBuf},
29 str::FromStr,
30};
31use sway_utils::{DEFAULT_IPFS_GATEWAY_URL, DEFAULT_REGISTRY_IPFS_GATEWAY_URL};
32
33trait Pin {
35 type Pinned: Fetch + Hash;
36 fn pin(&self, ctx: PinCtx) -> Result<(Self::Pinned, PathBuf)>;
37}
38
39trait Fetch {
41 fn fetch(&self, ctx: PinCtx, local: &Path) -> Result<PackageManifestFile>;
42}
43
44trait DepPath {
46 fn dep_path(&self, name: &str) -> Result<DependencyPath>;
47}
48
49type FetchId = u64;
50
51#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
52pub enum IPFSNode {
53 Local,
54 WithUrl(String),
55}
56
57impl Default for IPFSNode {
58 fn default() -> Self {
59 Self::WithUrl(DEFAULT_IPFS_GATEWAY_URL.to_string())
60 }
61}
62
63impl IPFSNode {
64 pub fn fuel() -> Self {
66 Self::WithUrl(DEFAULT_REGISTRY_IPFS_GATEWAY_URL.to_string())
67 }
68
69 pub fn public() -> Self {
71 Self::WithUrl(DEFAULT_IPFS_GATEWAY_URL.to_string())
72 }
73}
74
75impl FromStr for IPFSNode {
76 type Err = anyhow::Error;
77
78 fn from_str(value: &str) -> Result<Self, Self::Err> {
79 match value {
80 "PUBLIC" => {
81 let url = sway_utils::constants::DEFAULT_IPFS_GATEWAY_URL;
82 Ok(IPFSNode::WithUrl(url.to_string()))
83 }
84 "FUEL" => {
85 let url = sway_utils::constants::DEFAULT_REGISTRY_IPFS_GATEWAY_URL;
86 Ok(IPFSNode::WithUrl(url.to_string()))
87 }
88 "LOCAL" => Ok(IPFSNode::Local),
89 url => Ok(IPFSNode::WithUrl(url.to_string())),
90 }
91 }
92}
93
94#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
102pub enum Source {
103 Member(member::Source),
105 Git(git::Source),
107 Path(path::Source),
109 Ipfs(ipfs::Source),
111 Registry(reg::Source),
113}
114
115#[derive(Clone, Debug, Eq, Hash, PartialEq, Deserialize, Serialize)]
120pub enum Pinned {
121 Member(member::Pinned),
122 Git(git::Pinned),
123 Path(path::Pinned),
124 Ipfs(ipfs::Pinned),
125 Registry(reg::Pinned),
126}
127
128#[derive(Clone)]
129pub(crate) struct PinCtx<'a> {
130 pub(crate) fetch_id: FetchId,
134 pub(crate) path_root: PinnedId,
136 pub(crate) offline: bool,
138 pub(crate) name: &'a str,
140 pub(crate) ipfs_node: &'a IPFSNode,
142}
143
144pub(crate) enum DependencyPath {
145 Member,
147 ManifestPath(PathBuf),
149 Root(PinnedId),
151}
152
153pub struct DisplayCompiling<'a, T> {
155 source: &'a T,
156 manifest_dir: &'a Path,
157}
158
159#[derive(Clone, Debug)]
161pub struct PinnedParseError;
162
163impl Source {
164 fn with_path_dependency(
166 relative_path: &Path,
167 manifest_dir: &Path,
168 member_manifests: &MemberManifestFiles,
169 ) -> Result<Self> {
170 let path = manifest_dir.join(relative_path);
171 let canonical_path = path
172 .canonicalize()
173 .map_err(|e| anyhow!("Failed to canonicalize dependency path {:?}: {}", path, e))?;
174 if member_manifests
176 .values()
177 .any(|pkg_manifest| pkg_manifest.dir() == canonical_path)
178 {
179 Ok(Source::Member(member::Source(canonical_path)))
180 } else {
181 Ok(Source::Path(canonical_path))
182 }
183 }
184
185 fn with_version_dependency(
187 pkg_name: &str,
188 version: &str,
189 namespace: ®::file_location::Namespace,
190 ) -> Result<Self> {
191 let semver = semver::Version::parse(version)?;
194 let source = reg::Source {
195 version: semver,
196 namespace: namespace.clone(),
197 name: pkg_name.to_string(),
198 };
199 Ok(Source::Registry(source))
200 }
201
202 pub fn from_manifest_dep(
204 manifest_dir: &Path,
205 dep_name: &str,
206 dep: &manifest::Dependency,
207 member_manifests: &MemberManifestFiles,
208 ) -> Result<Self> {
209 let source = match dep {
210 manifest::Dependency::Simple(ref ver_str) => Source::with_version_dependency(
211 dep_name,
212 ver_str,
213 ®::file_location::Namespace::Flat,
214 )?,
215 manifest::Dependency::Detailed(ref det) => {
216 match (&det.path, &det.version, &det.git, &det.ipfs) {
217 (Some(relative_path), _, _, _) => {
218 let relative_path = PathBuf::from_str(relative_path)?;
219 Source::with_path_dependency(
220 &relative_path,
221 manifest_dir,
222 member_manifests,
223 )?
224 }
225 (_, _, Some(repo), _) => {
226 let reference = match (&det.branch, &det.tag, &det.rev) {
227 (Some(branch), None, None) => git::Reference::Branch(branch.clone()),
228 (None, Some(tag), None) => git::Reference::Tag(tag.clone()),
229 (None, None, Some(rev)) => git::Reference::Rev(rev.clone()),
230 (None, None, None) => git::Reference::DefaultBranch,
231 _ => bail!(
232 "git dependencies support at most one reference: \
233 either `branch`, `tag` or `rev`"
234 ),
235 };
236 let repo = Url::from_str(repo)?;
237 let source = git::Source { repo, reference };
238 Source::Git(source)
239 }
240 (_, _, _, Some(ipfs)) => {
241 let cid = ipfs.parse()?;
242 let source = ipfs::Source(cid);
243 Source::Ipfs(source)
244 }
245 (None, Some(version), _, _) => {
246 let namespace = det.namespace.as_ref().map_or_else(
247 || reg::file_location::Namespace::Flat,
248 |ns| reg::file_location::Namespace::Domain(ns.to_string()),
249 );
250 Source::with_version_dependency(dep_name, version, &namespace)?
251 }
252 _ => {
253 bail!("unsupported set of fields for dependency: {:?}", dep);
254 }
255 }
256 }
257 };
258 Ok(source)
259 }
260
261 pub fn from_manifest_dep_patched(
265 manifest: &PackageManifestFile,
266 dep_name: &str,
267 dep: &manifest::Dependency,
268 members: &MemberManifestFiles,
269 ) -> Result<Self> {
270 let unpatched = Self::from_manifest_dep(manifest.dir(), dep_name, dep, members)?;
271 unpatched.apply_patch(dep_name, manifest, members)
272 }
273
274 fn dep_patch(
284 &self,
285 dep_name: &str,
286 manifest: &PackageManifestFile,
287 ) -> Result<Option<manifest::Dependency>> {
288 let check_patches = |patch_key: &str| -> Result<Option<manifest::Dependency>> {
290 let patches = manifest.resolve_patch(patch_key)?;
291 Ok(patches.and_then(|p| p.get(dep_name).cloned()))
292 };
293
294 match self {
295 Source::Git(git) => {
296 let git_url = git.repo.to_string();
297 check_patches(&git_url)
298 }
299 Source::Registry(reg_source) => {
300 if let reg::file_location::Namespace::Domain(ns) = ®_source.namespace {
302 let namespaced_key = format!("{}/{}", reg::REGISTRY_PATCH_KEY, ns);
303 if let Some(patch) = check_patches(&namespaced_key)? {
304 return Ok(Some(patch));
305 }
306 }
307
308 check_patches(reg::REGISTRY_PATCH_KEY)
310 }
311 _ => Ok(None),
312 }
313 }
314
315 pub fn apply_patch(
320 &self,
321 dep_name: &str,
322 manifest: &PackageManifestFile,
323 members: &MemberManifestFiles,
324 ) -> Result<Self> {
325 match self.dep_patch(dep_name, manifest)? {
326 Some(patch) => Self::from_manifest_dep(manifest.dir(), dep_name, &patch, members),
327 None => Ok(self.clone()),
328 }
329 }
330
331 pub(crate) fn pin(&self, ctx: PinCtx, manifests: &mut ManifestMap) -> Result<Pinned> {
338 fn f<T>(source: &T, ctx: PinCtx, manifests: &mut ManifestMap) -> Result<T::Pinned>
339 where
340 T: Pin,
341 T::Pinned: Clone,
342 Pinned: From<T::Pinned>,
343 {
344 let (pinned, fetch_path) = source.pin(ctx.clone())?;
345 let id = PinnedId::new(ctx.name(), &Pinned::from(pinned.clone()));
346 if let hash_map::Entry::Vacant(entry) = manifests.entry(id) {
347 entry.insert(pinned.fetch(ctx, &fetch_path)?);
348 }
349 Ok(pinned)
350 }
351 match self {
352 Source::Member(source) => Ok(Pinned::Member(f(source, ctx, manifests)?)),
353 Source::Path(source) => Ok(Pinned::Path(f(source, ctx, manifests)?)),
354 Source::Git(source) => Ok(Pinned::Git(f(source, ctx, manifests)?)),
355 Source::Ipfs(source) => Ok(Pinned::Ipfs(f(source, ctx, manifests)?)),
356 Source::Registry(source) => Ok(Pinned::Registry(f(source, ctx, manifests)?)),
357 }
358 }
359}
360
361impl Pinned {
362 pub(crate) const MEMBER: Self = Self::Member(member::Pinned);
363
364 pub(crate) fn dep_path(&self, name: &str) -> Result<DependencyPath> {
366 match self {
367 Self::Member(pinned) => pinned.dep_path(name),
368 Self::Path(pinned) => pinned.dep_path(name),
369 Self::Git(pinned) => pinned.dep_path(name),
370 Self::Ipfs(pinned) => pinned.dep_path(name),
371 Self::Registry(pinned) => pinned.dep_path(name),
372 }
373 }
374
375 pub fn semver(&self) -> Option<semver::Version> {
379 match self {
380 Self::Registry(reg) => Some(reg.source.version.clone()),
381 _ => None,
382 }
383 }
384
385 pub fn display_compiling<'a>(&'a self, manifest_dir: &'a Path) -> DisplayCompiling<'a, Self> {
392 DisplayCompiling {
393 source: self,
394 manifest_dir,
395 }
396 }
397
398 pub fn unpinned(&self, path: &Path) -> Source {
400 match self {
401 Self::Member(_) => Source::Member(member::Source(path.to_owned())),
402 Self::Git(git) => Source::Git(git.source.clone()),
403 Self::Path(_) => Source::Path(path.to_owned()),
404 Self::Ipfs(ipfs) => Source::Ipfs(ipfs::Source(ipfs.0.clone())),
405 Self::Registry(reg) => Source::Registry(reg.source.clone()),
406 }
407 }
408}
409
410impl<'a> PinCtx<'a> {
411 fn fetch_id(&self) -> FetchId {
412 self.fetch_id
413 }
414 fn path_root(&self) -> PinnedId {
415 self.path_root
416 }
417 fn offline(&self) -> bool {
418 self.offline
419 }
420 fn name(&self) -> &str {
421 self.name
422 }
423 fn ipfs_node(&self) -> &'a IPFSNode {
424 self.ipfs_node
425 }
426}
427
428impl fmt::Display for Pinned {
429 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
430 match self {
431 Self::Member(src) => src.fmt(f),
432 Self::Path(src) => src.fmt(f),
433 Self::Git(src) => src.fmt(f),
434 Self::Ipfs(src) => src.fmt(f),
435 Self::Registry(src) => src.fmt(f),
436 }
437 }
438}
439
440impl fmt::Display for DisplayCompiling<'_, Pinned> {
441 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
442 match self.source {
443 Pinned::Member(_) => self.manifest_dir.display().fmt(f),
444 Pinned::Path(_src) => self.manifest_dir.display().fmt(f),
445 Pinned::Git(src) => src.fmt(f),
446 Pinned::Ipfs(src) => src.fmt(f),
447 Pinned::Registry(src) => src.fmt(f),
448 }
449 }
450}
451
452impl FromStr for Pinned {
453 type Err = PinnedParseError;
454 fn from_str(s: &str) -> Result<Self, Self::Err> {
455 let source = if s == "root" || s == "member" {
458 Self::Member(member::Pinned)
459 } else if let Ok(src) = path::Pinned::from_str(s) {
460 Self::Path(src)
461 } else if let Ok(src) = git::Pinned::from_str(s) {
462 Self::Git(src)
463 } else if let Ok(src) = ipfs::Pinned::from_str(s) {
464 Self::Ipfs(src)
465 } else if let Ok(src) = reg::Pinned::from_str(s) {
466 Self::Registry(src)
467 } else {
468 return Err(PinnedParseError);
469 };
470 Ok(source)
471 }
472}
473
474pub fn fetch_id(path: &Path, timestamp: std::time::Instant) -> u64 {
479 let mut hasher = hash_map::DefaultHasher::new();
480 path.hash(&mut hasher);
481 timestamp.hash(&mut hasher);
482 hasher.finish()
483}
484
485#[cfg(test)]
486mod tests {
487 use super::*;
488 use crate::manifest::{Dependency, DependencyDetails};
489 use std::collections::BTreeMap;
490
491 fn create_test_manifest_file_with_patches(
493 patches: BTreeMap<String, BTreeMap<String, Dependency>>,
494 ) -> (tempfile::TempDir, PackageManifestFile) {
495 let mut toml_str = r#"[project]
497name = "test_pkg"
498license = "Apache-2.0"
499entry = "main.sw"
500implicit-std = false
501"#
502 .to_string();
503
504 if !patches.is_empty() {
506 toml_str.push('\n');
507 for (patch_key, patch_deps) in patches {
508 toml_str.push_str(&format!("[patch.'{}']\n", patch_key));
509 for (dep_name, dep) in patch_deps {
510 let dep_toml = match dep {
512 Dependency::Simple(ver) => format!(r#""{ver}""#),
513 Dependency::Detailed(det) => {
514 let mut parts = Vec::new();
515 if let Some(path) = &det.path {
516 parts.push(format!(r#"path = "{path}""#));
517 }
518 if let Some(git) = &det.git {
519 parts.push(format!(r#"git = "{git}""#));
520 }
521 if let Some(branch) = &det.branch {
522 parts.push(format!(r#"branch = "{branch}""#));
523 }
524 if let Some(tag) = &det.tag {
525 parts.push(format!(r#"tag = "{tag}""#));
526 }
527 if let Some(version) = &det.version {
528 parts.push(format!(r#"version = "{version}""#));
529 }
530 format!("{{ {} }}", parts.join(", "))
531 }
532 };
533 toml_str.push_str(&format!("{} = {}\n", dep_name, dep_toml));
534 }
535 }
536 }
537
538 let temp_dir = tempfile::tempdir().unwrap();
540 let src_dir = temp_dir.path().join("src");
541 std::fs::create_dir(&src_dir).unwrap();
542
543 let main_sw_path = src_dir.join("main.sw");
545 std::fs::write(&main_sw_path, "contract;").unwrap();
546
547 let manifest_path = temp_dir.path().join("Forc.toml");
549 std::fs::write(&manifest_path, toml_str).unwrap();
550
551 let manifest_file = PackageManifestFile::from_file(&manifest_path).unwrap();
553
554 (temp_dir, manifest_file)
555 }
556
557 fn path_dep(path: &str) -> Dependency {
559 Dependency::Detailed(DependencyDetails {
560 path: Some(path.to_string()),
561 ..Default::default()
562 })
563 }
564
565 fn git_dep(repo: &str, branch: &str) -> Dependency {
567 Dependency::Detailed(DependencyDetails {
568 git: Some(repo.to_string()),
569 branch: Some(branch.to_string()),
570 ..Default::default()
571 })
572 }
573
574 #[test]
575 fn test_registry_patch_flat_namespace() {
576 let source = Source::Registry(reg::Source {
578 name: "std".to_string(),
579 version: semver::Version::new(0, 63, 0),
580 namespace: reg::file_location::Namespace::Flat,
581 });
582
583 let mut patches = BTreeMap::new();
585 let mut forc_pub_patches = BTreeMap::new();
586 forc_pub_patches.insert("std".to_string(), path_dep("../local-std"));
587 patches.insert("forc.pub".to_string(), forc_pub_patches);
588
589 let (_temp_dir, manifest_file) = create_test_manifest_file_with_patches(patches);
590
591 let patch = source.dep_patch("std", &manifest_file).unwrap();
593 assert!(
594 patch.is_some(),
595 "Should find patch for flat namespace registry dependency"
596 );
597
598 let patch = patch.unwrap();
599 match patch {
600 Dependency::Detailed(det) => {
601 assert_eq!(det.path, Some("../local-std".to_string()));
602 }
603 _ => panic!("Expected detailed dependency"),
604 }
605 }
606
607 #[test]
608 fn test_registry_patch_domain_namespace() {
609 let source = Source::Registry(reg::Source {
611 name: "fuel-core".to_string(),
612 version: semver::Version::new(1, 0, 0),
613 namespace: reg::file_location::Namespace::Domain("com/fuel".to_string()),
614 });
615
616 let mut patches = BTreeMap::new();
618 let mut namespaced_patches = BTreeMap::new();
619 namespaced_patches.insert("fuel-core".to_string(), path_dep("../local-fuel-core"));
620 patches.insert("forc.pub/com/fuel".to_string(), namespaced_patches);
621
622 let (_temp_dir, manifest_file) = create_test_manifest_file_with_patches(patches);
623
624 let patch = source.dep_patch("fuel-core", &manifest_file).unwrap();
626 assert!(
627 patch.is_some(),
628 "Should find patch for domain namespace registry dependency"
629 );
630
631 let patch = patch.unwrap();
632 match patch {
633 Dependency::Detailed(det) => {
634 assert_eq!(det.path, Some("../local-fuel-core".to_string()));
635 }
636 _ => panic!("Expected detailed dependency"),
637 }
638 }
639
640 #[test]
641 fn test_registry_patch_namespace_priority() {
642 let source = Source::Registry(reg::Source {
644 name: "my-lib".to_string(),
645 version: semver::Version::new(2, 0, 0),
646 namespace: reg::file_location::Namespace::Domain("com/myorg".to_string()),
647 });
648
649 let mut patches = BTreeMap::new();
651
652 let mut namespaced_patches = BTreeMap::new();
654 namespaced_patches.insert("my-lib".to_string(), path_dep("../namespaced-lib"));
655 patches.insert("forc.pub/com/myorg".to_string(), namespaced_patches);
656
657 let mut generic_patches = BTreeMap::new();
659 generic_patches.insert("my-lib".to_string(), path_dep("../generic-lib"));
660 patches.insert("forc.pub".to_string(), generic_patches);
661
662 let (_temp_dir, manifest_file) = create_test_manifest_file_with_patches(patches);
663
664 let patch = source.dep_patch("my-lib", &manifest_file).unwrap();
666 assert!(patch.is_some(), "Should find patch");
667
668 let patch = patch.unwrap();
669 match patch {
670 Dependency::Detailed(det) => {
671 assert_eq!(
672 det.path,
673 Some("../namespaced-lib".to_string()),
674 "Should use namespace-specific patch, not generic patch"
675 );
676 }
677 _ => panic!("Expected detailed dependency"),
678 }
679 }
680
681 #[test]
682 fn test_registry_patch_fallback_to_generic() {
683 let source = Source::Registry(reg::Source {
685 name: "common-lib".to_string(),
686 version: semver::Version::new(1, 0, 0),
687 namespace: reg::file_location::Namespace::Domain("com/myorg".to_string()),
688 });
689
690 let mut patches = BTreeMap::new();
692 let mut generic_patches = BTreeMap::new();
693 generic_patches.insert("common-lib".to_string(), path_dep("../common-lib"));
694 patches.insert("forc.pub".to_string(), generic_patches);
695
696 let (_temp_dir, manifest_file) = create_test_manifest_file_with_patches(patches);
697
698 let patch = source.dep_patch("common-lib", &manifest_file).unwrap();
700 assert!(patch.is_some(), "Should find generic patch as fallback");
701
702 let patch = patch.unwrap();
703 match patch {
704 Dependency::Detailed(det) => {
705 assert_eq!(det.path, Some("../common-lib".to_string()));
706 }
707 _ => panic!("Expected detailed dependency"),
708 }
709 }
710
711 #[test]
712 fn test_git_patch_still_works() {
713 let repo_url = "https://github.com/fuellabs/sway";
715 let source = Source::Git(git::Source {
716 repo: git::Url::from_str(repo_url).unwrap(),
717 reference: git::Reference::Tag("v0.63.0".to_string()),
718 });
719
720 let mut patches = BTreeMap::new();
722 let mut git_patches = BTreeMap::new();
723 git_patches.insert(
724 "std".to_string(),
725 git_dep("https://github.com/fuellabs/sway", "feature-branch"),
726 );
727 patches.insert(repo_url.to_string(), git_patches);
728
729 let (_temp_dir, manifest_file) = create_test_manifest_file_with_patches(patches);
730
731 let patch = source.dep_patch("std", &manifest_file).unwrap();
733 assert!(
734 patch.is_some(),
735 "Should find git patch (backward compatibility)"
736 );
737
738 let patch = patch.unwrap();
739 match patch {
740 Dependency::Detailed(det) => {
741 assert_eq!(
742 det.git,
743 Some("https://github.com/fuellabs/sway".to_string())
744 );
745 assert_eq!(det.branch, Some("feature-branch".to_string()));
746 }
747 _ => panic!("Expected detailed dependency"),
748 }
749 }
750
751 #[test]
752 fn test_no_patch_found() {
753 let source = Source::Registry(reg::Source {
755 name: "no-patch-lib".to_string(),
756 version: semver::Version::new(1, 0, 0),
757 namespace: reg::file_location::Namespace::Flat,
758 });
759
760 let mut patches = BTreeMap::new();
762 let mut forc_pub_patches = BTreeMap::new();
763 forc_pub_patches.insert("other-lib".to_string(), path_dep("../other-lib"));
764 patches.insert("forc.pub".to_string(), forc_pub_patches);
765
766 let (_temp_dir, manifest_file) = create_test_manifest_file_with_patches(patches);
767
768 let patch = source.dep_patch("no-patch-lib", &manifest_file).unwrap();
770 assert!(
771 patch.is_none(),
772 "Should not find patch for different package"
773 );
774 }
775
776 #[test]
777 fn test_path_source_no_patch() {
778 let source = Source::Path(PathBuf::from("/some/path"));
780
781 let (_temp_dir, manifest_file) = create_test_manifest_file_with_patches(BTreeMap::new());
782
783 let patch = source.dep_patch("anything", &manifest_file).unwrap();
785 assert!(patch.is_none(), "Path sources should not support patches");
786 }
787}