Skip to main content

nickel_lang_package/
lock.rs

1//! Lock files and lock file utilities
2
3use std::{
4    collections::{BTreeMap, HashMap},
5    num::ParseIntError,
6    path::{Path, PathBuf},
7};
8
9use gix::ObjectId;
10use serde::{Deserialize, Serialize};
11use serde_with::FromInto;
12
13use crate::{
14    Dependency, GitDependency, ManifestFile, PreciseGitPkg, PreciseIndexPkg, PrecisePkg,
15    error::{Error, IoResultExt},
16    index::{self},
17    resolve::Resolution,
18    version::SemVer,
19};
20
21/// We need to give names to entries in the lock-file, so that we can refer to dependencies.
22///
23/// It's tempting to try to use `Precise` for identifying dependencies, but we can't because
24/// the lock file doesn't store paths on the local filesystem. So we identify packages by
25/// the name that its importer uses, and add numbers to ensure uniqueness.
26#[derive(
27    Clone,
28    Debug,
29    Default,
30    serde_with::SerializeDisplay,
31    serde_with::DeserializeFromStr,
32    PartialEq,
33    Eq,
34    PartialOrd,
35    Ord,
36    Hash,
37)]
38pub struct EntryName {
39    pub name: String,
40    pub id: u32,
41}
42
43impl std::fmt::Display for EntryName {
44    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45        if self.id == 0 {
46            write!(f, "{}", self.name)
47        } else {
48            // There's no ambiguity here because the `std.package.Manifest`
49            // contract prohibits package names with spaces.
50            write!(f, "{} {}", self.name, self.id)
51        }
52    }
53}
54
55impl std::str::FromStr for EntryName {
56    type Err = ParseIntError;
57
58    fn from_str(s: &str) -> Result<EntryName, ParseIntError> {
59        if let Some((name, num)) = s.split_once(' ') {
60            Ok(EntryName {
61                name: name.to_owned(),
62                id: num.parse()?,
63            })
64        } else {
65            Ok(EntryName {
66                name: s.to_owned(),
67                id: 0,
68            })
69        }
70    }
71}
72
73#[derive(Default)]
74struct LockFileNamer {
75    counts: HashMap<String, u32>,
76    assigned: HashMap<PrecisePkg, (String, u32)>,
77}
78
79impl LockFileNamer {
80    fn name(&mut self, name: &str, pkg: &PrecisePkg) -> EntryName {
81        if let Some((old_name, old_num)) = self.assigned.get(pkg) {
82            EntryName {
83                name: old_name.to_owned(),
84                id: *old_num,
85            }
86        } else {
87            let count = *self
88                .counts
89                .entry(name.to_owned())
90                .and_modify(|i| *i += 1)
91                .or_insert(0);
92            self.assigned.insert(pkg.clone(), (name.to_owned(), count));
93            EntryName {
94                name: name.to_owned(),
95                id: count,
96            }
97        }
98    }
99}
100
101/// A lock file, specifying versions and names for all recursive dependencies.
102///
103/// This defines the on-disk format for lock files.
104///
105/// # Open question
106///
107/// There's one big open question about the lock file: should it contain information
108/// about path dependencies (and their recursive dependencies)? If it does, you
109/// can immediately derive the `PackageMap` from the lock file, meaning that if the
110/// interpreter gets the lock file then it can do everything else from there,
111/// without doing any package resolution. So that's nice.
112///
113/// The problem with putting information about path dependencies in the lock file is
114/// that path dependencies can change without notice, making the lock file stale.
115/// So the interpreter didn't have to do much work, but it ended up running on old
116/// information.
117///
118/// I think the decision here basically comes down to what we want from the CLI
119/// interface. If we require a separate update-the-lock-file step (a la npm or poetry),
120/// it makes sense to put the path dependency info here. But if we want an
121/// auto-refresh step (a la cargo), we want to leave it out.
122#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
123pub struct LockFile {
124    /// The dependencies of the current (top-level) package.
125    ///
126    /// These should be sorted so that the serialization doesn't change all the time.
127    pub dependencies: BTreeMap<String, LockFileDep>,
128    /// All packages that we know about, and the dependencies of each one.
129    ///
130    /// Note that the package list is not guaranteed to be closed: path dependencies
131    /// cannot have their dependencies resolved in the on-disk lockfile because they
132    /// can change at any time. *Some* path dependencies (for example, path dependencies
133    /// that are local to a git dependency repo) may have resolved dependencies.
134    pub packages: BTreeMap<EntryName, LockFileEntry>,
135}
136
137impl LockFile {
138    pub fn empty() -> Self {
139        Self {
140            dependencies: BTreeMap::new(),
141            packages: BTreeMap::new(),
142        }
143    }
144
145    pub fn new(manifest: &ManifestFile, resolution: &Resolution) -> Result<Self, Error> {
146        fn collect_packages(
147            resolution: &Resolution,
148            id: &str,
149            pkg: &PrecisePkg,
150            acc: &mut BTreeMap<EntryName, LockFileEntry>,
151            namer: &mut LockFileNamer,
152        ) -> Result<EntryName, Error> {
153            let name = namer.name(id, pkg);
154            let entry = LockFileEntry {
155                precise: pkg.clone().into(),
156                dependencies: resolution
157                    .sorted_dependencies(pkg)?
158                    .into_iter()
159                    .map(|(id, dep, precise)| {
160                        let spec = match dep {
161                            Dependency::Git(g) => Some(g.clone()),
162                            Dependency::Path(_) => None,
163                            Dependency::Index(_) => None,
164                        };
165                        let entry = LockFileDep {
166                            name: namer.name(id.label(), &precise),
167                            spec,
168                        };
169                        (id.label().to_owned(), entry)
170                    })
171                    .collect(),
172            };
173
174            // Only recurse if this is the first time we've encountered this precise package.
175            if acc.insert(name.clone(), entry).is_none() {
176                for (id, _dep, precise) in resolution.sorted_dependencies(pkg)? {
177                    collect_packages(resolution, id.label(), &precise, acc, namer)?;
178                }
179            }
180            Ok(name)
181        }
182
183        let mut acc = BTreeMap::new();
184
185        let mut dependencies = BTreeMap::new();
186        let mut namer = LockFileNamer::default();
187        for (id, dep) in manifest.sorted_dependencies() {
188            let pkg = resolution.precise(dep);
189            let name = collect_packages(resolution, id, &pkg, &mut acc, &mut namer)?;
190            let spec = match dep {
191                Dependency::Git(g) => Some(g.clone()),
192                Dependency::Path(_) => None,
193                Dependency::Index(_) => None,
194            };
195            let entry = LockFileDep { name, spec };
196            dependencies.insert(id.to_owned(), entry);
197        }
198
199        Ok(LockFile {
200            dependencies,
201            packages: acc,
202        })
203    }
204
205    /// Read a lock file from disk.
206    pub fn from_path(path: impl AsRef<Path>) -> Result<Self, Error> {
207        let path = path.as_ref();
208        let contents = std::fs::read_to_string(path).with_path(path)?;
209        serde_json::from_str(&contents).map_err(|error| Error::LockFileDeserialization {
210            path: path.to_owned(),
211            error,
212        })
213    }
214
215    /// Write out this lock file to the filesystem.
216    pub fn write(&self, path: &Path) -> Result<(), Error> {
217        // unwrap: serde_json serialization fails if the derived `Serialize`
218        // trait fails (which it shouldn't), or if there's a map with
219        // non-string keys (all our maps have `Ident` keys).
220        let serialized_lock = serde_json::to_string_pretty(self).unwrap();
221        std::fs::write(path, serialized_lock).with_path(path)?;
222        Ok(())
223    }
224
225    pub fn dependency<'a>(
226        &'a self,
227        entry: Option<&'a LockFileEntry>,
228        name: &str,
229    ) -> Option<&'a LockFileDep> {
230        match entry {
231            None => self.dependencies.get(name),
232            Some(entry) => entry.dependencies.get(name),
233        }
234    }
235}
236
237/// A precise package version, in a format suitable for putting into a lockfile.
238///
239/// This is like `crate::Precise`, but doesn't store paths. (We remember that there
240/// was a path, but not what it was.)
241#[serde_with::serde_as]
242#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
243pub enum LockPrecisePkg {
244    Git {
245        // We use `Precise` for a few different purposes, and not all of them need the url. (For
246        // resolution, for example, we could consider two git deps equal if they have the same id
247        // even if they came from different sources.) However, the lockfile should have a repo url in
248        // it, because it allows us to fetch the package if it isn't available, and it allows us to
249        // check if the locked dependency matches the manifest (which might only have the url).
250        #[serde(with = "crate::serde_url")]
251        url: gix::Url,
252        // Serialize/deserialize as hex strings.
253        #[serde_as(as = "serde_with::DisplayFromStr")]
254        id: ObjectId,
255        path: PathBuf,
256    },
257    Path,
258    Index {
259        #[serde_as(as = "FromInto<crate::index::serialize::IdFormat>")]
260        id: index::Id,
261        version: SemVer,
262    },
263}
264
265impl From<PrecisePkg> for LockPrecisePkg {
266    fn from(p: PrecisePkg) -> Self {
267        match p {
268            // We don't currently prevent leaking local paths that point to git repos. Should we?
269            PrecisePkg::Git(PreciseGitPkg { url, id, path }) => {
270                LockPrecisePkg::Git { url, id, path }
271            }
272            PrecisePkg::Path { .. } => LockPrecisePkg::Path,
273            PrecisePkg::Index(PreciseIndexPkg { id, version }) => {
274                LockPrecisePkg::Index { id, version }
275            }
276        }
277    }
278}
279
280#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
281pub struct LockFileDep {
282    pub name: EntryName,
283    /// For git packages, we store their original git spec in the lock-file, so
284    /// that if someone changes the spec in the manifest we can tell that we
285    /// need to re-fetch the repo.
286    ///
287    /// Note that this goes in `LockFileDep` (which represents the dependency
288    /// edge between two entries) rather than `LockFileEntry` because there can
289    /// be multiple git specs that end up resolving to the same git revision.
290    #[serde(skip_serializing_if = "Option::is_none")]
291    #[serde(default)]
292    pub spec: Option<GitDependency>,
293}
294
295/// The dependencies of a single package.
296#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
297pub struct LockFileEntry {
298    pub precise: LockPrecisePkg,
299    pub dependencies: BTreeMap<String, LockFileDep>,
300}