Skip to main content

nickel_lang_package/
lib.rs

1// TODO: many (most?) of our error variants are large. This might be
2// automatically solved by converting to the new runtime representation,
3// but if not then we should reduce them.
4#![allow(clippy::result_large_err)]
5
6use std::path::{Path, PathBuf};
7
8use gix::{Url, bstr::ByteSlice as _};
9use index::{LockType, PackageIndex};
10use lock::{LockFileDep, LockPrecisePkg};
11use nickel_lang_core::cache::normalize_abs_path;
12
13use config::Config;
14use error::Error;
15use serde::{Deserialize, Serialize};
16
17macro_rules! warn {
18    ($($tts:tt)*) => {
19        eprintln!($($tts)*);
20    }
21}
22
23macro_rules! info {
24    ($($tts:tt)*) => {
25        eprintln!($($tts)*);
26    }
27}
28
29pub mod config;
30pub mod error;
31pub mod index;
32pub mod lock;
33pub mod manifest;
34pub mod resolve;
35pub mod snapshot;
36pub mod version;
37
38pub use gix::ObjectId;
39pub use manifest::ManifestFile;
40use version::{SemVer, VersionReq};
41
42/// A dependency that comes from a git repository.
43#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
44pub struct GitDependency {
45    /// The url of the git repo, in any format understood by `gix`.
46    /// For example, it can be a path, an https url, or an ssh url.
47    #[serde(with = "serde_url")]
48    pub url: gix::Url,
49    #[serde(default, rename = "ref")]
50    pub target: nickel_lang_git::Target,
51    /// The path to the nickel package within the git repo, if it is not at the top level.
52    #[serde(default)]
53    pub path: PathBuf,
54}
55
56impl GitDependency {
57    /// If this git dependency specifies a relative path, make it absolute.
58    pub fn relative_to(&self, relative_to: Option<&Path>) -> Result<Self, Error> {
59        if self.url.scheme.as_str() != "file" {
60            return Ok(self.clone());
61        }
62        // unwrap: the url ultimately came from a nickel file, which is always valid UTF-8.
63        let path = Path::new(self.url.path.to_str().unwrap());
64        if path.is_absolute() {
65            return Ok(self.clone());
66        }
67
68        match relative_to {
69            Some(relative_to) => {
70                let abs_path = relative_to.join(path);
71                Ok(GitDependency {
72                    url: Url::try_from(abs_path.as_path())?,
73                    ..self.clone()
74                })
75            }
76            None => Err(Error::RelativeGitImport {
77                path: path.to_owned(),
78            }),
79        }
80    }
81}
82
83#[derive(Clone, Debug, PartialEq, Eq, Hash)]
84/// A dependency that comes from the global package index.
85pub struct IndexDependency {
86    pub id: index::Id,
87    pub version: VersionReq,
88}
89
90/// A source includes the place to fetch a package from (e.g. git or a registry),
91/// along with possibly some narrowing-down of the allowed versions (e.g. a range
92/// of versions, or a git commit id).
93#[derive(Clone, Debug, PartialEq, Eq, Hash)]
94pub enum Dependency {
95    Git(GitDependency),
96    Path(PathBuf),
97    Index(IndexDependency),
98}
99
100impl Dependency {
101    pub fn matches(&self, entry: &LockFileDep, precise: &LockPrecisePkg) -> bool {
102        match self {
103            Dependency::Git(git) => match &entry.spec {
104                Some(locked_git) => git == locked_git,
105                None => false,
106            },
107            Dependency::Path(_) => true,
108            Dependency::Index(i) => {
109                if let LockPrecisePkg::Index { id, version } = precise {
110                    i.id == *id && i.version.matches(version)
111                } else {
112                    false
113                }
114            }
115        }
116    }
117
118    pub fn as_index_dep(self, parent_id: &index::Id) -> Result<IndexDependency, Error> {
119        match self {
120            Dependency::Index(i) => Ok(i),
121            Dependency::Git(g) => Err(Error::InvalidIndexDep {
122                id: parent_id.clone(),
123                dep: Box::new(crate::UnversionedDependency::Git(g)),
124            }),
125            Dependency::Path(path) => Err(Error::InvalidIndexDep {
126                id: parent_id.clone(),
127                dep: Box::new(crate::UnversionedDependency::Path(path)),
128            }),
129        }
130    }
131
132    pub fn as_unversioned(self) -> Option<UnversionedDependency> {
133        match self {
134            Dependency::Index(_i) => None,
135            Dependency::Git(g) => Some(UnversionedDependency::Git(g)),
136            Dependency::Path(p) => Some(UnversionedDependency::Path(p)),
137        }
138    }
139}
140
141/// The subtype of [`Dependency`] containing just the git and path variants.
142///
143/// We call these "unversioned" because they don't have version numbers that get
144/// decided during resolution.
145#[derive(Clone, PartialEq, Eq, Hash, Debug)]
146pub enum UnversionedDependency {
147    Git(GitDependency),
148    Path(PathBuf),
149}
150
151impl From<UnversionedDependency> for Dependency {
152    fn from(p: UnversionedDependency) -> Self {
153        match p {
154            UnversionedDependency::Git(git) => Dependency::Git(git),
155            UnversionedDependency::Path(path) => Dependency::Path(path),
156        }
157    }
158}
159
160mod serde_url {
161    use serde::{Deserialize, Serialize as _, de::Error};
162
163    pub fn serialize<S: serde::Serializer>(url: &gix::Url, ser: S) -> Result<S::Ok, S::Error> {
164        // unwrap: locked urls can only come from nickel strings in the manifest file, which must be
165        // valid utf-8
166        std::str::from_utf8(url.to_bstring().as_slice())
167            .unwrap()
168            .serialize(ser)
169    }
170
171    pub fn deserialize<'de, D: serde::Deserializer<'de>>(de: D) -> Result<gix::Url, D::Error> {
172        let s = String::deserialize(de)?;
173        gix::Url::try_from(s).map_err(|e| D::Error::custom(e.to_string()))
174    }
175}
176
177/// A package that comes from the global package index, with a precise version.
178#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
179pub struct PreciseIndexPkg {
180    pub id: index::Id,
181    pub version: SemVer,
182}
183
184impl PreciseIndexPkg {
185    /// Where on the local filesystem can we find the root of the git repository
186    /// containing this package?
187    ///
188    /// We don't currently support git filters, so for index packages that live
189    /// in a subdirectory of a git repository we fetch the whole repository
190    /// (into the path returned by this method). The package itself lives in a
191    /// subdirectory; see [`PreciseIndexPkg::local_path`] for that one.
192    pub fn local_path_without_subdir<T: LockType>(
193        &self,
194        config: &Config,
195        index: &PackageIndex<T>,
196    ) -> Result<PathBuf, Error> {
197        let pkg = index.package(&self.id, &self.version)?;
198
199        Ok(config
200            .index_package_dir
201            .join("contents")
202            .join(pkg.id.object_id().to_string()))
203    }
204
205    /// Where on the local filesystem can this package be found?
206    //
207    /// Note that the package might not actually be there yet.
208    pub fn local_path<T: LockType>(
209        &self,
210        config: &Config,
211        index: &PackageIndex<T>,
212    ) -> Result<PathBuf, Error> {
213        let index::Id::Github { path, .. } = &self.id;
214        Ok(self.local_path_without_subdir(config, index)?.join(path))
215    }
216}
217
218/// A git package, with a precise version.
219#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
220pub struct PreciseGitPkg {
221    url: gix::Url,
222    id: ObjectId,
223    path: PathBuf,
224}
225
226impl PreciseGitPkg {
227    /// Where on the local filesystem can this package be found?
228    //
229    /// Note that the package might not actually be there yet.
230    pub fn local_path(&self, config: &Config) -> PathBuf {
231        repo_root(config, &self.id).join(&self.path)
232    }
233}
234
235/// A precise package version, in a format suitable for putting into a lockfile.
236#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
237pub enum PrecisePkg {
238    /// A git package.
239    Git(PreciseGitPkg),
240    /// The path is normalized (i.e., all '..'s are at the beginning), and
241    /// relative to the top-level package manifest. (Technically, it could be an
242    /// absolute path if that's what they wrote in the manifest. But we haven't
243    /// *turned* it into an absolute path.)
244    ///
245    /// Note that when normalizing we only look at the path and not at the actual filesystem.
246    Path(PathBuf),
247    /// A package in the global package index.
248    Index(PreciseIndexPkg),
249}
250
251impl PrecisePkg {
252    /// Where on the local filesystem can this package be found?
253    ///
254    /// Note that the package might not actually be there yet, if it's a git or
255    /// index package that hasn't been fetched.
256    pub fn local_path<T: LockType>(
257        &self,
258        config: &Config,
259        index: &PackageIndex<T>,
260    ) -> Result<PathBuf, Error> {
261        match self {
262            PrecisePkg::Git(pkg) => Ok(pkg.local_path(config)),
263            PrecisePkg::Path(path) => Ok(path.clone()),
264            PrecisePkg::Index(PreciseIndexPkg { id, version }) => {
265                let pkg = index.package(id, version)?;
266                let index::Id::Github { path, .. } = id;
267                Ok(config
268                    .index_package_dir
269                    .join("contents")
270                    .join(pkg.id.object_id().to_string())
271                    .join(path))
272            }
273        }
274    }
275
276    /// Is this a path package?
277    pub fn is_path(&self) -> bool {
278        matches!(self, PrecisePkg::Path { .. })
279    }
280
281    /// Is this package available offline? If not, it needs to be fetched.
282    pub fn is_available_offline<T: LockType>(
283        &self,
284        config: &Config,
285        index: &PackageIndex<T>,
286    ) -> bool {
287        // We consider path-dependencies to be always available offline, even if they don't exist.
288        // We consider git-dependencies to be available offline if there's a directory at
289        // `~/.cache/nickel/git/ed8234.../` (or wherever the cache directory is on your system). We
290        // don't check if that directory contains the right git repository -- if someone has messed
291        // with the contents of `~/.cache/nickel`, that's your problem.
292        match self {
293            PrecisePkg::Path { .. } => true,
294            _ => self.local_path(config, index).is_ok_and(|p| p.is_dir()),
295        }
296    }
297
298    /// If this is a path package with a relative path, turn it into an absolute path, relative to `root`.
299    pub fn with_abs_path(self, root: &std::path::Path) -> Self {
300        match self {
301            PrecisePkg::Path(path) => PrecisePkg::Path(normalize_abs_path(&root.join(path))),
302            x => x,
303        }
304    }
305
306    pub fn unversioned_or_index(self) -> Result<UnversionedPrecisePkg, PreciseIndexPkg> {
307        match self {
308            PrecisePkg::Git(g) => Ok(UnversionedPrecisePkg::Git(g)),
309            PrecisePkg::Path(p) => Ok(UnversionedPrecisePkg::Path(p)),
310            PrecisePkg::Index(i) => Err(i),
311        }
312    }
313}
314
315/// A precise package version, but only the ones that don't have versions to resolve.
316#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
317pub enum UnversionedPrecisePkg {
318    Git(PreciseGitPkg),
319    Path(PathBuf),
320}
321
322impl UnversionedPrecisePkg {
323    pub fn local_path(&self, config: &Config) -> PathBuf {
324        match self {
325            Self::Git(PreciseGitPkg { id, path, .. }) => repo_root(config, id).join(path),
326            Self::Path(path) => path.clone(),
327        }
328    }
329}
330
331impl From<UnversionedPrecisePkg> for PrecisePkg {
332    fn from(uv: UnversionedPrecisePkg) -> PrecisePkg {
333        match uv {
334            UnversionedPrecisePkg::Git(g) => PrecisePkg::Git(g),
335            UnversionedPrecisePkg::Path(p) => PrecisePkg::Path(p),
336        }
337    }
338}
339
340/// The path in our local filesystem where we store the git repo with the given id.
341fn repo_root(config: &Config, id: &ObjectId) -> PathBuf {
342    config.git_package_dir.join(id.to_string())
343}