Skip to main content

lex_syntax/
lock.rs

1//! `lex.lock` — the resolved, reproducible pin file for registry
2//! dependencies (#893).
3//!
4//! A `lex.toml` registry dependency states a *constraint*
5//! (`{ registry = "…", version = "^1.2" }`); the registry publishes a set of
6//! immutable releases (#911). Resolution (`lex pkg lock`, in `lex-cli`) picks
7//! the highest release satisfying the constraint and records the exact choice
8//! — version *and* the op-log head it points at — here, so a later
9//! `resolve_package_import` fetches that precise version instead of drifting
10//! to whatever is newest.
11//!
12//! The *format* lives in `lex-syntax` (next to [`crate::workspace`] and
13//! [`crate::semver`]) because it is the resolved form of the manifest: package
14//! resolution reads it, and the CLI writes it. The *resolution policy* (which
15//! release to pick, how to fetch the version list) stays in `lex-cli`.
16
17use serde::{Deserialize, Serialize};
18use std::path::Path;
19
20/// Current lockfile format version.
21pub const LOCK_FORMAT_VERSION: u32 = 1;
22
23/// One resolved dependency: the constraint that produced it and the exact
24/// release it pins to.
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
26pub struct LockEntry {
27    pub name: String,
28    pub registry: String,
29    /// The `version = "…"` constraint from `lex.toml`, recorded so `lex pkg
30    /// lock` can tell whether an existing pin still satisfies its declaration.
31    pub constraint: String,
32    /// The concrete `MAJOR.MINOR.PATCH` release chosen.
33    pub version: String,
34    /// The op-log head that release points at (#911). `None` if the registry
35    /// served a release with no head — resolution still pins the version.
36    #[serde(default, skip_serializing_if = "Option::is_none")]
37    pub head_op: Option<String>,
38}
39
40/// The whole `lex.lock` file.
41#[derive(Debug, Clone, Default, Serialize, Deserialize)]
42pub struct LockFile {
43    /// Lockfile format version, so a future change can migrate rather than
44    /// silently misread.
45    pub version: u32,
46    #[serde(default, rename = "package")]
47    pub packages: Vec<LockEntry>,
48}
49
50impl LockFile {
51    /// The recorded pin for `name`, if any.
52    pub fn entry(&self, name: &str) -> Option<&LockEntry> {
53        self.packages.iter().find(|e| e.name == name)
54    }
55
56    /// Load `<dir>/lex.lock`, or `None` if it is absent or unreadable. A
57    /// malformed lock is treated as absent — resolution falls back to the
58    /// declared version and reports if that is itself unusable.
59    pub fn load_dir(dir: &Path) -> Option<LockFile> {
60        let raw = std::fs::read_to_string(dir.join("lex.lock")).ok()?;
61        Self::from_toml(&raw).ok()
62    }
63
64    /// Parse a `lex.lock` from a TOML string.
65    pub fn from_toml(s: &str) -> Result<Self, String> {
66        toml::from_str(s).map_err(|e| format!("parsing lex.lock: {e}"))
67    }
68
69    /// Render to the on-disk TOML, with a do-not-edit header. Entries are
70    /// sorted by name so the file is stable across runs (a churning lock is
71    /// noise in review).
72    pub fn to_toml(&self) -> Result<String, String> {
73        let mut sorted = self.clone();
74        sorted.packages.sort_by(|a, b| a.name.cmp(&b.name));
75        let body = toml::to_string(&sorted).map_err(|e| format!("serializing lex.lock: {e}"))?;
76        Ok(format!(
77            "# lex.lock — resolved dependency pins, generated by `lex pkg lock`.\n\
78             # Do not edit by hand; run `lex pkg lock` (or `lex pkg update`) instead.\n\n{body}"
79        ))
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn toml_round_trips_and_sorts_by_name() {
89        let lf = LockFile {
90            version: LOCK_FORMAT_VERSION,
91            packages: vec![
92                LockEntry {
93                    name: "zeta".into(),
94                    registry: "reg".into(),
95                    constraint: "^1".into(),
96                    version: "1.0.0".into(),
97                    head_op: Some("op_z".into()),
98                },
99                LockEntry {
100                    name: "alpha".into(),
101                    registry: "reg".into(),
102                    constraint: "^2".into(),
103                    version: "2.1.0".into(),
104                    head_op: None,
105                },
106            ],
107        };
108        let toml = lf.to_toml().unwrap();
109        assert!(toml.find("alpha").unwrap() < toml.find("zeta").unwrap());
110        let back = LockFile::from_toml(&toml).unwrap();
111        assert_eq!(back.version, LOCK_FORMAT_VERSION);
112        assert_eq!(back.entry("alpha").unwrap().head_op, None);
113        assert_eq!(back.entry("zeta").unwrap().version, "1.0.0");
114    }
115}