pint_pkg/
source.rs

1//! Implementations for the different kinds of dependency sources.
2
3use crate::{
4    manifest::{self, ManifestFile},
5    plan::{PinnedId, PinnedManifests},
6};
7use serde::{Deserialize, Serialize};
8use std::{
9    collections::hash_map,
10    fmt,
11    hash::{Hash, Hasher},
12    path::{Path, PathBuf},
13};
14use thiserror::Error;
15
16mod member;
17mod path;
18
19/// Pin this source at a specific "version", return the local directory to fetch into.
20trait Pin {
21    type Pinned: Fetch + Hash;
22    type Error: fmt::Debug + fmt::Display;
23    fn pin(&self, ctx: PinCtx) -> Result<(Self::Pinned, PathBuf), Self::Error>;
24}
25
26/// Fetch (and optionally cache) a pinned instance of this source to the given path.
27trait Fetch {
28    type Error: fmt::Debug + fmt::Display;
29    fn fetch(&self, ctx: PinCtx, local: &Path) -> Result<ManifestFile, Self::Error>;
30}
31
32/// The canonical, local path for this source as a dependency.
33trait DepPath {
34    type Error: fmt::Debug + fmt::Display;
35    fn dep_path(&self, name: &str) -> Result<DependencyPath, Self::Error>;
36}
37
38/// Represents the source for a package.
39///
40/// The `Source` type does not specify a speccific, pinned version, but does
41/// specify a method of retrieving the pinned version.
42#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
43pub enum Source {
44    /// Used to refer to one of the workspace members.
45    Member(member::Source),
46    /// A path to a directory with a `pint.toml` manifest at its root.
47    Path(path::Source),
48}
49
50// The pinned form of a package source.
51#[derive(Clone, Debug, Eq, Hash, PartialEq, Deserialize, Serialize)]
52pub enum Pinned {
53    Member(member::Pinned),
54    Path(path::Pinned),
55}
56
57/// The context provided to the pinning and fetching of a source type.
58#[derive(Clone)]
59pub(crate) struct PinCtx<'a> {
60    /// A unique ID associated with the current fetch.
61    // TODO: Use this for remote deps.
62    pub(crate) _fetch_id: FetchId,
63    /// The current package graph path root.
64    pub(crate) path_root: PinnedId,
65    /// The name of the package being pinned.
66    pub(crate) pkg_name: &'a str,
67}
68
69pub(crate) enum DependencyPath {
70    /// The dependency is another member of the workspace.
71    Member,
72    /// The dependency is located at this specific path.
73    // TODO: Rm this `allow` when introducing remote deps.
74    #[allow(dead_code)]
75    ManifestPath(PathBuf),
76    /// Path is pinned via manifest, relative to the given root node.
77    Root(PinnedId),
78}
79
80/// Error indicating failure to construct `Source` from manifest dependency.
81#[derive(Debug, Error)]
82pub enum SourceError {
83    #[error("failed to canonicalize path {0:?}: {1}")]
84    FailedToCanonicalizePath(std::path::PathBuf, std::io::Error),
85}
86
87/// Failed to pin or fetch the source of a dependency.
88#[derive(Debug, Error)]
89pub enum PinAndFetchError {
90    /// Failed to pin and fetch a path dependency.
91    #[error("{0}")]
92    Path(#[from] PinAndFetchErrorKind<path::Source>),
93    /// Failed to pin and fetch a member dependency.
94    #[error("{0}")]
95    Member(#[from] PinAndFetchErrorKind<member::Source>),
96}
97
98/// Failed to pin or fetch the source of a particular dependency source type.
99#[derive(Debug, Error)]
100#[allow(private_bounds, private_interfaces)]
101pub enum PinAndFetchErrorKind<T: Pin> {
102    #[error("failed to pin dependency source: {0}")]
103    Pin(T::Error),
104    #[error("failed to fetch dependency source: {0}")]
105    Fetch(<T::Pinned as Fetch>::Error),
106}
107
108/// Failed to resolve the dependency's path.
109#[derive(Debug, Error)]
110#[error("failed to resolve the path to the dependency's local source")]
111pub enum DepPathError {}
112
113pub type FetchId = u64;
114
115impl Source {
116    /// Constructs either a `Path` or a `Member` source from the given relative path.
117    ///
118    /// The returned `Source` is a `Member` in the case that the canonical path
119    /// matches one of the manifest paths in the given `member_manifests` set.
120    fn from_relative_path<'a>(
121        manifest_dir: &Path,
122        relative_path: &Path,
123        member_manifests: impl IntoIterator<Item = &'a ManifestFile>,
124    ) -> Result<Self, SourceError> {
125        let path = manifest_dir.join(relative_path);
126        let canonical_path = path
127            .canonicalize()
128            .map_err(|e| SourceError::FailedToCanonicalizePath(path, e))?;
129        let is_member = member_manifests
130            .into_iter()
131            .any(|pkg_manifest| pkg_manifest.dir() == canonical_path);
132        if is_member {
133            Ok(Source::Member(member::Source(canonical_path)))
134        } else {
135            Ok(Source::Path(canonical_path))
136        }
137    }
138
139    /// Construct a `Source` from the given manifest depenency.
140    pub fn from_manifest_dep<'a>(
141        manifest_dir: &Path,
142        dep: &manifest::Dependency,
143        member_manifests: impl IntoIterator<Item = &'a ManifestFile>,
144    ) -> Result<Self, SourceError> {
145        match &dep.source {
146            manifest::dependency::Source::Path(path) => {
147                Self::from_relative_path(manifest_dir, &path.path, member_manifests)
148            }
149        }
150    }
151
152    /// Determine the pinned version of a dependency from its source.
153    ///
154    /// If a manifest does not yet exist for the pinned version, fetch the
155    /// dependency into its expected location and add its manifest to the pinned
156    /// manifests map.
157    pub(crate) fn pin_and_fetch(
158        &self,
159        ctx: PinCtx,
160        manifests: &mut PinnedManifests,
161    ) -> Result<Pinned, PinAndFetchError> {
162        match self {
163            Source::Member(source) => Ok(Pinned::Member(pin_and_fetch(source, ctx, manifests)?)),
164            Source::Path(source) => Ok(Pinned::Path(pin_and_fetch(source, ctx, manifests)?)),
165        }
166    }
167}
168
169impl Pinned {
170    /// Short-hand for `Pinned::Member(member::Pinned)`.
171    pub const MEMBER: Self = Self::Member(member::Pinned);
172
173    /// Retrieve the unpinned instance of this source.
174    pub fn unpinned(&self, path: &Path) -> Source {
175        match self {
176            Self::Member(_) => Source::Member(member::Source(path.to_owned())),
177            Self::Path(_) => Source::Path(path.to_owned()),
178        }
179    }
180
181    /// Return how the pinned source for a dependency can be found on the local file system.
182    pub(crate) fn dep_path(&self, name: &str) -> Result<DependencyPath, DepPathError> {
183        match self {
184            Self::Member(pinned) => Ok(pinned.dep_path(name).expect("infallible")),
185            Self::Path(pinned) => Ok(pinned.dep_path(name).expect("infallible")),
186        }
187    }
188}
189
190impl fmt::Display for Pinned {
191    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
192        match self {
193            Self::Member(p) => p.fmt(f),
194            Self::Path(p) => p.fmt(f),
195        }
196    }
197}
198
199fn pin_and_fetch<T>(
200    source: &T,
201    ctx: PinCtx,
202    manifests: &mut PinnedManifests,
203) -> Result<T::Pinned, PinAndFetchErrorKind<T>>
204where
205    T: Pin,
206    T::Pinned: Clone,
207    Pinned: From<T::Pinned>,
208{
209    let (pinned, fetch_path) = source.pin(ctx.clone()).map_err(PinAndFetchErrorKind::Pin)?;
210    let id = PinnedId::new(ctx.pkg_name, &Pinned::from(pinned.clone()));
211    if let hash_map::Entry::Vacant(entry) = manifests.entry(id) {
212        let res = pinned.fetch(ctx, &fetch_path);
213        let manifest = res.map_err(PinAndFetchErrorKind::Fetch)?;
214        entry.insert(manifest);
215    }
216    Ok(pinned)
217}
218
219/// A unique ID for a fetch pass.
220pub fn fetch_graph_id(path: &Path, timestamp: std::time::Instant) -> FetchId {
221    let mut hasher = hash_map::DefaultHasher::new();
222    path.hash(&mut hasher);
223    timestamp.hash(&mut hasher);
224    hasher.finish()
225}