forc_pkg/source/
mod.rs

1//! Related to pinning, fetching, validating and caching the source for packages.
2//!
3//! To add a new source kind:
4//!
5//! 1. Add a new module.
6//! 2. Create types providing implementations for each of the traits in this module.
7//! 3. Add a variant to the `Source` and `Pinned` types in this module.
8//! 4. Add variant support to the `from_manifest_dep` and `FromStr` implementations.
9
10pub 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
33/// Pin this source at a specific "version", return the local directory to fetch into.
34trait Pin {
35    type Pinned: Fetch + Hash;
36    fn pin(&self, ctx: PinCtx) -> Result<(Self::Pinned, PathBuf)>;
37}
38
39/// Fetch (and optionally cache) a pinned instance of this source to the given path.
40trait Fetch {
41    fn fetch(&self, ctx: PinCtx, local: &Path) -> Result<PackageManifestFile>;
42}
43
44/// Given a parent manifest, return the canonical, local path for this source as a dependency.
45trait 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    /// Returns an IPFSNode configured to use the Fuel-operated IPFS gateway.
65    pub fn fuel() -> Self {
66        Self::WithUrl(DEFAULT_REGISTRY_IPFS_GATEWAY_URL.to_string())
67    }
68
69    /// Returns an IPFSNode configured to use the public IPFS gateway.
70    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/// Specifies a base source for a package.
95///
96/// - For registry packages, this includes a base version.
97/// - For git packages, this includes a base git reference like a branch or tag.
98///
99/// Note that a `Source` does not specify a specific, pinned version. Rather, it specifies a source
100/// at which the current latest version may be located.
101#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
102pub enum Source {
103    /// Used to refer to a workspace member project.
104    Member(member::Source),
105    /// A git repo with a `Forc.toml` manifest at its root.
106    Git(git::Source),
107    /// A path to a directory with a `Forc.toml` manifest at its root.
108    Path(path::Source),
109    /// A package described by its IPFS CID.
110    Ipfs(ipfs::Source),
111    /// A forc project hosted on the official registry.
112    Registry(reg::Source),
113}
114
115/// A pinned instance of the package source.
116///
117/// Specifies an exact version to use, or an exact commit in the case of git dependencies. The
118/// pinned version or commit is updated upon creation of the lock file and on `forc update`.
119#[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    /// A unique hash associated with the process' current fetch pass.
131    /// NOTE: Only to be used for creating temporary directories. Should not
132    /// interact with anything that appears in the pinned output.
133    pub(crate) fetch_id: FetchId,
134    /// Within the context of a package graph fetch traversal, represents the current path root.
135    pub(crate) path_root: PinnedId,
136    /// Whether or not the fetch is occurring offline.
137    pub(crate) offline: bool,
138    /// The name of the package associated with this source.
139    pub(crate) name: &'a str,
140    /// The IPFS node to use for fetching IPFS sources.
141    pub(crate) ipfs_node: &'a IPFSNode,
142}
143
144pub(crate) enum DependencyPath {
145    /// The dependency is another member of the workspace.
146    Member,
147    /// The dependency is located at this specific path.
148    ManifestPath(PathBuf),
149    /// Path is pinned via manifest, relative to the given root node.
150    Root(PinnedId),
151}
152
153/// A wrapper type for providing `Display` implementations for compiling msgs.
154pub struct DisplayCompiling<'a, T> {
155    source: &'a T,
156    manifest_dir: &'a Path,
157}
158
159/// Error returned upon failed parsing of `SourcePinned::from_str`.
160#[derive(Clone, Debug)]
161pub struct PinnedParseError;
162
163impl Source {
164    /// Construct a source from path information collected from manifest file.
165    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        // Check if path is a member of a workspace.
175        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    /// Construct a source from version information collected from manifest file.
186    fn with_version_dependency(
187        pkg_name: &str,
188        version: &str,
189        namespace: &reg::file_location::Namespace,
190    ) -> Result<Self> {
191        // TODO: update here once we are supporting non-exact versions (non `x.y.z` versions)
192        // see: https://github.com/FuelLabs/sway/issues/7060
193        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    /// Convert the given manifest `Dependency` declaration to a `Source`.
203    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                &reg::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    /// Convert the given manifest `Dependency` declaration to a source,
262    /// applying any relevant patches from within the given `manifest` as
263    /// necessary.
264    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    /// If a patch exists for this dependency source within the given project
275    /// manifest, this returns the patch.
276    fn dep_patch(
277        &self,
278        dep_name: &str,
279        manifest: &PackageManifestFile,
280    ) -> Result<Option<manifest::Dependency>> {
281        if let Source::Git(git) = self {
282            if let Some(patches) = manifest.resolve_patch(&git.repo.to_string())? {
283                if let Some(patch) = patches.get(dep_name) {
284                    return Ok(Some(patch.clone()));
285                }
286            }
287        }
288        Ok(None)
289    }
290
291    /// If a patch exists for the dependency associated with this source within
292    /// the given manifest, this returns a new `Source` with the patch applied.
293    ///
294    /// If no patch exists, this returns the original `Source`.
295    pub fn apply_patch(
296        &self,
297        dep_name: &str,
298        manifest: &PackageManifestFile,
299        members: &MemberManifestFiles,
300    ) -> Result<Self> {
301        match self.dep_patch(dep_name, manifest)? {
302            Some(patch) => Self::from_manifest_dep(manifest.dir(), dep_name, &patch, members),
303            None => Ok(self.clone()),
304        }
305    }
306
307    /// Attempt to determine the pinned version or commit for the source.
308    ///
309    /// Also updates the manifest map with a path to the local copy of the pkg.
310    ///
311    /// The `path_root` is required for `Path` dependencies and must specify the package that is the
312    /// root of the current subgraph of path dependencies.
313    pub(crate) fn pin(&self, ctx: PinCtx, manifests: &mut ManifestMap) -> Result<Pinned> {
314        fn f<T>(source: &T, ctx: PinCtx, manifests: &mut ManifestMap) -> Result<T::Pinned>
315        where
316            T: Pin,
317            T::Pinned: Clone,
318            Pinned: From<T::Pinned>,
319        {
320            let (pinned, fetch_path) = source.pin(ctx.clone())?;
321            let id = PinnedId::new(ctx.name(), &Pinned::from(pinned.clone()));
322            if let hash_map::Entry::Vacant(entry) = manifests.entry(id) {
323                entry.insert(pinned.fetch(ctx, &fetch_path)?);
324            }
325            Ok(pinned)
326        }
327        match self {
328            Source::Member(source) => Ok(Pinned::Member(f(source, ctx, manifests)?)),
329            Source::Path(source) => Ok(Pinned::Path(f(source, ctx, manifests)?)),
330            Source::Git(source) => Ok(Pinned::Git(f(source, ctx, manifests)?)),
331            Source::Ipfs(source) => Ok(Pinned::Ipfs(f(source, ctx, manifests)?)),
332            Source::Registry(source) => Ok(Pinned::Registry(f(source, ctx, manifests)?)),
333        }
334    }
335}
336
337impl Pinned {
338    pub(crate) const MEMBER: Self = Self::Member(member::Pinned);
339
340    /// Return how the pinned source for a dependency can be found on the local file system.
341    pub(crate) fn dep_path(&self, name: &str) -> Result<DependencyPath> {
342        match self {
343            Self::Member(pinned) => pinned.dep_path(name),
344            Self::Path(pinned) => pinned.dep_path(name),
345            Self::Git(pinned) => pinned.dep_path(name),
346            Self::Ipfs(pinned) => pinned.dep_path(name),
347            Self::Registry(pinned) => pinned.dep_path(name),
348        }
349    }
350
351    /// If the source is associated with a specific semver version, emit it.
352    ///
353    /// Used solely for the package lock file.
354    pub fn semver(&self) -> Option<semver::Version> {
355        match self {
356            Self::Registry(reg) => Some(reg.source.version.clone()),
357            _ => None,
358        }
359    }
360
361    /// Wrap `self` in some type able to be formatted for the compiling output.
362    ///
363    /// This refers to `<source>` in the following:
364    /// ```ignore
365    /// Compiling <kind> <name> (<source>)
366    /// ```
367    pub fn display_compiling<'a>(&'a self, manifest_dir: &'a Path) -> DisplayCompiling<'a, Self> {
368        DisplayCompiling {
369            source: self,
370            manifest_dir,
371        }
372    }
373
374    /// Retrieve the unpinned instance of this source.
375    pub fn unpinned(&self, path: &Path) -> Source {
376        match self {
377            Self::Member(_) => Source::Member(member::Source(path.to_owned())),
378            Self::Git(git) => Source::Git(git.source.clone()),
379            Self::Path(_) => Source::Path(path.to_owned()),
380            Self::Ipfs(ipfs) => Source::Ipfs(ipfs::Source(ipfs.0.clone())),
381            Self::Registry(reg) => Source::Registry(reg.source.clone()),
382        }
383    }
384}
385
386impl<'a> PinCtx<'a> {
387    fn fetch_id(&self) -> FetchId {
388        self.fetch_id
389    }
390    fn path_root(&self) -> PinnedId {
391        self.path_root
392    }
393    fn offline(&self) -> bool {
394        self.offline
395    }
396    fn name(&self) -> &str {
397        self.name
398    }
399    fn ipfs_node(&self) -> &'a IPFSNode {
400        self.ipfs_node
401    }
402}
403
404impl fmt::Display for Pinned {
405    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
406        match self {
407            Self::Member(src) => src.fmt(f),
408            Self::Path(src) => src.fmt(f),
409            Self::Git(src) => src.fmt(f),
410            Self::Ipfs(src) => src.fmt(f),
411            Self::Registry(src) => src.fmt(f),
412        }
413    }
414}
415
416impl fmt::Display for DisplayCompiling<'_, Pinned> {
417    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
418        match self.source {
419            Pinned::Member(_) => self.manifest_dir.display().fmt(f),
420            Pinned::Path(_src) => self.manifest_dir.display().fmt(f),
421            Pinned::Git(src) => src.fmt(f),
422            Pinned::Ipfs(src) => src.fmt(f),
423            Pinned::Registry(src) => src.fmt(f),
424        }
425    }
426}
427
428impl FromStr for Pinned {
429    type Err = PinnedParseError;
430    fn from_str(s: &str) -> Result<Self, Self::Err> {
431        // Also check `"root"` to support reading the legacy `Forc.lock` format and to
432        // avoid breaking old projects.
433        let source = if s == "root" || s == "member" {
434            Self::Member(member::Pinned)
435        } else if let Ok(src) = path::Pinned::from_str(s) {
436            Self::Path(src)
437        } else if let Ok(src) = git::Pinned::from_str(s) {
438            Self::Git(src)
439        } else if let Ok(src) = ipfs::Pinned::from_str(s) {
440            Self::Ipfs(src)
441        } else if let Ok(src) = reg::Pinned::from_str(s) {
442            Self::Registry(src)
443        } else {
444            return Err(PinnedParseError);
445        };
446        Ok(source)
447    }
448}
449
450/// Produce a unique ID for a particular fetch pass.
451///
452/// This is used in the temporary git directory and allows for avoiding contention over the git
453/// repo directory.
454pub fn fetch_id(path: &Path, timestamp: std::time::Instant) -> u64 {
455    let mut hasher = hash_map::DefaultHasher::new();
456    path.hash(&mut hasher);
457    timestamp.hash(&mut hasher);
458    hasher.finish()
459}