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;
14mod 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;
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)]
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 FromStr for IPFSNode {
64    type Err = anyhow::Error;
65
66    fn from_str(value: &str) -> Result<Self, Self::Err> {
67        match value {
68            "PUBLIC" => {
69                let url = sway_utils::constants::DEFAULT_IPFS_GATEWAY_URL;
70                Ok(IPFSNode::WithUrl(url.to_string()))
71            }
72            "LOCAL" => Ok(IPFSNode::Local),
73            url => Ok(IPFSNode::WithUrl(url.to_string())),
74        }
75    }
76}
77
78/// Specifies a base source for a package.
79///
80/// - For registry packages, this includes a base version.
81/// - For git packages, this includes a base git reference like a branch or tag.
82///
83/// Note that a `Source` does not specify a specific, pinned version. Rather, it specifies a source
84/// at which the current latest version may be located.
85#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
86pub enum Source {
87    /// Used to refer to a workspace member project.
88    Member(member::Source),
89    /// A git repo with a `Forc.toml` manifest at its root.
90    Git(git::Source),
91    /// A path to a directory with a `Forc.toml` manifest at its root.
92    Path(path::Source),
93    /// A package described by its IPFS CID.
94    Ipfs(ipfs::Source),
95    /// A forc project hosted on the official registry.
96    Registry(reg::Source),
97}
98
99/// A pinned instance of the package source.
100///
101/// Specifies an exact version to use, or an exact commit in the case of git dependencies. The
102/// pinned version or commit is updated upon creation of the lock file and on `forc update`.
103#[derive(Clone, Debug, Eq, Hash, PartialEq, Deserialize, Serialize)]
104pub enum Pinned {
105    Member(member::Pinned),
106    Git(git::Pinned),
107    Path(path::Pinned),
108    Ipfs(ipfs::Pinned),
109    Registry(reg::Pinned),
110}
111
112#[derive(Clone)]
113pub(crate) struct PinCtx<'a> {
114    /// A unique hash associated with the process' current fetch pass.
115    /// NOTE: Only to be used for creating temporary directories. Should not
116    /// interact with anything that appears in the pinned output.
117    pub(crate) fetch_id: FetchId,
118    /// Within the context of a package graph fetch traversal, represents the current path root.
119    pub(crate) path_root: PinnedId,
120    /// Whether or not the fetch is occurring offline.
121    pub(crate) offline: bool,
122    /// The name of the package associated with this source.
123    pub(crate) name: &'a str,
124    /// The IPFS node to use for fetching IPFS sources.
125    pub(crate) ipfs_node: &'a IPFSNode,
126}
127
128pub(crate) enum DependencyPath {
129    /// The dependency is another member of the workspace.
130    Member,
131    /// The dependency is located at this specific path.
132    ManifestPath(PathBuf),
133    /// Path is pinned via manifest, relative to the given root node.
134    Root(PinnedId),
135}
136
137/// A wrapper type for providing `Display` implementations for compiling msgs.
138pub struct DisplayCompiling<'a, T> {
139    source: &'a T,
140    manifest_dir: &'a Path,
141}
142
143/// Error returned upon failed parsing of `SourcePinned::from_str`.
144#[derive(Clone, Debug)]
145pub struct PinnedParseError;
146
147impl Source {
148    /// Convert the given manifest `Dependency` declaration to a `Source`.
149    pub fn from_manifest_dep(
150        manifest_dir: &Path,
151        dep: &manifest::Dependency,
152        member_manifests: &MemberManifestFiles,
153    ) -> Result<Self> {
154        let source = match dep {
155            manifest::Dependency::Simple(ref ver_str) => {
156                bail!(
157                    "Unsupported dependency declaration in \"{}\": `{}` - \
158                    currently only `git` and `path` dependencies are supported",
159                    manifest_dir.display(),
160                    ver_str
161                )
162            }
163            manifest::Dependency::Detailed(ref det) => {
164                match (&det.path, &det.version, &det.git, &det.ipfs) {
165                    (Some(relative_path), _, _, _) => {
166                        let path = manifest_dir.join(relative_path);
167                        let canonical_path = path.canonicalize().map_err(|e| {
168                            anyhow!("Failed to canonicalize dependency path {:?}: {}", path, e)
169                        })?;
170                        // Check if path is a member of a workspace.
171                        if member_manifests
172                            .values()
173                            .any(|pkg_manifest| pkg_manifest.dir() == canonical_path)
174                        {
175                            Source::Member(member::Source(canonical_path))
176                        } else {
177                            Source::Path(canonical_path)
178                        }
179                    }
180                    (_, _, Some(repo), _) => {
181                        let reference = match (&det.branch, &det.tag, &det.rev) {
182                            (Some(branch), None, None) => git::Reference::Branch(branch.clone()),
183                            (None, Some(tag), None) => git::Reference::Tag(tag.clone()),
184                            (None, None, Some(rev)) => git::Reference::Rev(rev.clone()),
185                            (None, None, None) => git::Reference::DefaultBranch,
186                            _ => bail!(
187                                "git dependencies support at most one reference: \
188                                either `branch`, `tag` or `rev`"
189                            ),
190                        };
191                        let repo = Url::from_str(repo)?;
192                        let source = git::Source { repo, reference };
193                        Source::Git(source)
194                    }
195                    (_, _, _, Some(ipfs)) => {
196                        let cid = ipfs.parse()?;
197                        let source = ipfs::Source(cid);
198                        Source::Ipfs(source)
199                    }
200                    _ => {
201                        bail!("unsupported set of fields for dependency: {:?}", dep);
202                    }
203                }
204            }
205        };
206        Ok(source)
207    }
208
209    /// Convert the given manifest `Dependency` declaration to a source,
210    /// applying any relevant patches from within the given `manifest` as
211    /// necessary.
212    pub fn from_manifest_dep_patched(
213        manifest: &PackageManifestFile,
214        dep_name: &str,
215        dep: &manifest::Dependency,
216        members: &MemberManifestFiles,
217    ) -> Result<Self> {
218        let unpatched = Self::from_manifest_dep(manifest.dir(), dep, members)?;
219        unpatched.apply_patch(dep_name, manifest, members)
220    }
221
222    /// If a patch exists for this dependency source within the given project
223    /// manifest, this returns the patch.
224    fn dep_patch(
225        &self,
226        dep_name: &str,
227        manifest: &PackageManifestFile,
228    ) -> Result<Option<manifest::Dependency>> {
229        if let Source::Git(git) = self {
230            if let Some(patches) = manifest.resolve_patch(&git.repo.to_string())? {
231                if let Some(patch) = patches.get(dep_name) {
232                    return Ok(Some(patch.clone()));
233                }
234            }
235        }
236        Ok(None)
237    }
238
239    /// If a patch exists for the dependency associated with this source within
240    /// the given manifest, this returns a new `Source` with the patch applied.
241    ///
242    /// If no patch exists, this returns the original `Source`.
243    pub fn apply_patch(
244        &self,
245        dep_name: &str,
246        manifest: &PackageManifestFile,
247        members: &MemberManifestFiles,
248    ) -> Result<Self> {
249        match self.dep_patch(dep_name, manifest)? {
250            Some(patch) => Self::from_manifest_dep(manifest.dir(), &patch, members),
251            None => Ok(self.clone()),
252        }
253    }
254
255    /// Attempt to determine the pinned version or commit for the source.
256    ///
257    /// Also updates the manifest map with a path to the local copy of the pkg.
258    ///
259    /// The `path_root` is required for `Path` dependencies and must specify the package that is the
260    /// root of the current subgraph of path dependencies.
261    pub(crate) fn pin(&self, ctx: PinCtx, manifests: &mut ManifestMap) -> Result<Pinned> {
262        fn f<T>(source: &T, ctx: PinCtx, manifests: &mut ManifestMap) -> Result<T::Pinned>
263        where
264            T: Pin,
265            T::Pinned: Clone,
266            Pinned: From<T::Pinned>,
267        {
268            let (pinned, fetch_path) = source.pin(ctx.clone())?;
269            let id = PinnedId::new(ctx.name(), &Pinned::from(pinned.clone()));
270            if let hash_map::Entry::Vacant(entry) = manifests.entry(id) {
271                entry.insert(pinned.fetch(ctx, &fetch_path)?);
272            }
273            Ok(pinned)
274        }
275        match self {
276            Source::Member(source) => Ok(Pinned::Member(f(source, ctx, manifests)?)),
277            Source::Path(source) => Ok(Pinned::Path(f(source, ctx, manifests)?)),
278            Source::Git(source) => Ok(Pinned::Git(f(source, ctx, manifests)?)),
279            Source::Ipfs(source) => Ok(Pinned::Ipfs(f(source, ctx, manifests)?)),
280            Source::Registry(source) => Ok(Pinned::Registry(f(source, ctx, manifests)?)),
281        }
282    }
283}
284
285impl Pinned {
286    pub(crate) const MEMBER: Self = Self::Member(member::Pinned);
287
288    /// Return how the pinned source for a dependency can be found on the local file system.
289    pub(crate) fn dep_path(&self, name: &str) -> Result<DependencyPath> {
290        match self {
291            Self::Member(pinned) => pinned.dep_path(name),
292            Self::Path(pinned) => pinned.dep_path(name),
293            Self::Git(pinned) => pinned.dep_path(name),
294            Self::Ipfs(pinned) => pinned.dep_path(name),
295            Self::Registry(pinned) => pinned.dep_path(name),
296        }
297    }
298
299    /// If the source is associated with a specific semver version, emit it.
300    ///
301    /// Used solely for the package lock file.
302    pub fn semver(&self) -> Option<semver::Version> {
303        match self {
304            Self::Registry(reg) => Some(reg.source.version.clone()),
305            _ => None,
306        }
307    }
308
309    /// Wrap `self` in some type able to be formatted for the compiling output.
310    ///
311    /// This refers to `<source>` in the following:
312    /// ```ignore
313    /// Compiling <kind> <name> (<source>)
314    /// ```
315    pub fn display_compiling<'a>(&'a self, manifest_dir: &'a Path) -> DisplayCompiling<'a, Self> {
316        DisplayCompiling {
317            source: self,
318            manifest_dir,
319        }
320    }
321
322    /// Retrieve the unpinned instance of this source.
323    pub fn unpinned(&self, path: &Path) -> Source {
324        match self {
325            Self::Member(_) => Source::Member(member::Source(path.to_owned())),
326            Self::Git(git) => Source::Git(git.source.clone()),
327            Self::Path(_) => Source::Path(path.to_owned()),
328            Self::Ipfs(ipfs) => Source::Ipfs(ipfs::Source(ipfs.0.clone())),
329            Self::Registry(reg) => Source::Registry(reg.source.clone()),
330        }
331    }
332}
333
334impl<'a> PinCtx<'a> {
335    fn fetch_id(&self) -> FetchId {
336        self.fetch_id
337    }
338    fn path_root(&self) -> PinnedId {
339        self.path_root
340    }
341    fn offline(&self) -> bool {
342        self.offline
343    }
344    fn name(&self) -> &str {
345        self.name
346    }
347    fn ipfs_node(&self) -> &'a IPFSNode {
348        self.ipfs_node
349    }
350}
351
352impl fmt::Display for Pinned {
353    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
354        match self {
355            Self::Member(src) => src.fmt(f),
356            Self::Path(src) => src.fmt(f),
357            Self::Git(src) => src.fmt(f),
358            Self::Ipfs(src) => src.fmt(f),
359            Self::Registry(_reg) => todo!("pkg registries not yet implemented"),
360        }
361    }
362}
363
364impl fmt::Display for DisplayCompiling<'_, Pinned> {
365    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
366        match self.source {
367            Pinned::Member(_) => self.manifest_dir.display().fmt(f),
368            Pinned::Path(_src) => self.manifest_dir.display().fmt(f),
369            Pinned::Git(src) => src.fmt(f),
370            Pinned::Ipfs(src) => src.fmt(f),
371            Pinned::Registry(_src) => todo!("registry dependencies not yet implemented"),
372        }
373    }
374}
375
376impl FromStr for Pinned {
377    type Err = PinnedParseError;
378    fn from_str(s: &str) -> Result<Self, Self::Err> {
379        // Also check `"root"` to support reading the legacy `Forc.lock` format and to
380        // avoid breaking old projects.
381        let source = if s == "root" || s == "member" {
382            Self::Member(member::Pinned)
383        } else if let Ok(src) = path::Pinned::from_str(s) {
384            Self::Path(src)
385        } else if let Ok(src) = git::Pinned::from_str(s) {
386            Self::Git(src)
387        } else if let Ok(src) = ipfs::Pinned::from_str(s) {
388            Self::Ipfs(src)
389        } else {
390            // TODO: Try parse registry source.
391            return Err(PinnedParseError);
392        };
393        Ok(source)
394    }
395}
396
397/// Produce a unique ID for a particular fetch pass.
398///
399/// This is used in the temporary git directory and allows for avoiding contention over the git
400/// repo directory.
401pub fn fetch_id(path: &Path, timestamp: std::time::Instant) -> u64 {
402    let mut hasher = hash_map::DefaultHasher::new();
403    path.hash(&mut hasher);
404    timestamp.hash(&mut hasher);
405    hasher.finish()
406}