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}