Skip to main content

leo_package/
lock.rs

1// Copyright (C) 2019-2026 Provable Inc.
2// This file is part of the Leo library.
3
4// The Leo library is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// The Leo library is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with the Leo library. If not, see <https://www.gnu.org/licenses/>.
16
17//! The `leo.lock` lock file, which pins git dependencies to exact commits.
18//!
19//! Only git dependencies need locking (network deps are pinned by edition, path deps by location).
20//! Entries are keyed by `(name, git, reference)`, so changing the requested reference re-resolves.
21
22use leo_errors::Result;
23
24use serde::{Deserialize, Serialize};
25use std::path::Path;
26
27/// File name of the lock file, stored alongside `program.json`.
28pub const LOCK_FILENAME: &str = "leo.lock";
29
30const LOCK_VERSION: u32 = 1;
31
32/// A single pinned git dependency.
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34pub struct GitLockEntry {
35    pub name: String,
36    pub git: String,
37    /// The requested reference in stable string form (see `GitReference::lock_string`).
38    pub reference: String,
39    pub commit: String,
40}
41
42/// The contents of `leo.lock`.
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct Lock {
45    version: u32,
46    #[serde(default)]
47    git: Vec<GitLockEntry>,
48}
49
50impl Default for Lock {
51    fn default() -> Self {
52        Lock { version: LOCK_VERSION, git: Vec::new() }
53    }
54}
55
56impl Lock {
57    /// Read the lock from `dir`, or an empty lock if it is missing. A malformed or unsupported-version
58    /// lock is regenerated rather than erroring, but warns (unlike a missing file) so lost pins are visible.
59    pub fn read(dir: &Path) -> Self {
60        let path = dir.join(LOCK_FILENAME);
61        let Ok(contents) = std::fs::read_to_string(&path) else {
62            return Lock::default();
63        };
64        match serde_json::from_str::<Lock>(&contents) {
65            Ok(lock) if lock.version == LOCK_VERSION => lock,
66            Ok(lock) => {
67                tracing::warn!(
68                    "⚠️ Ignoring `{}`: unsupported lock version {} (expected {LOCK_VERSION}). It will be regenerated.",
69                    path.display(),
70                    lock.version,
71                );
72                Lock::default()
73            }
74            Err(err) => {
75                tracing::warn!("⚠️ Ignoring malformed `{}` ({err}). It will be regenerated.", path.display());
76                Lock::default()
77            }
78        }
79    }
80
81    /// The pinned commit for `(name, git, reference)`, or `None` (forcing re-resolution) on any mismatch.
82    pub fn commit_for(&self, name: &str, git: &str, reference: &str) -> Option<&str> {
83        self.git.iter().find(|e| e.name == name && e.git == git && e.reference == reference).map(|e| e.commit.as_str())
84    }
85
86    /// The commit any dependency resolved `(git, reference)` to, regardless of name. Lets a build
87    /// reuse a resolution it already performed for another dependency on the same repository.
88    pub fn commit_for_source(&self, git: &str, reference: &str) -> Option<&str> {
89        self.git.iter().find(|e| e.git == git && e.reference == reference).map(|e| e.commit.as_str())
90    }
91
92    /// Record a pinned commit, replacing any existing entry for the same `(name, git, reference)`.
93    /// Entries under other references are kept: in a shared workspace lock they may belong to
94    /// another member; stale ones are pruned by `carry_over` or `leo remove`.
95    pub fn record(&mut self, name: String, git: String, reference: String, commit: String) {
96        self.git.retain(|e| !(e.name == name && e.git == git && e.reference == reference));
97        self.git.push(GitLockEntry { name, git, reference, commit });
98    }
99
100    /// Carry over entries from `old` that were not re-recorded in this lock and that `keep` accepts.
101    pub fn carry_over(&mut self, old: &Lock, mut keep: impl FnMut(&GitLockEntry) -> bool) {
102        for entry in &old.git {
103            if self.commit_for(&entry.name, &entry.git, &entry.reference).is_none() && keep(entry) {
104                self.git.push(entry.clone());
105            }
106        }
107    }
108
109    /// Remove all entries pinning the dependency `name`.
110    pub fn remove_name(&mut self, name: &str) {
111        self.git.retain(|e| e.name != name);
112    }
113
114    /// Write the lock to `dir`, entries sorted for determinism. With no git dependencies, no file is
115    /// written and a stale one is removed.
116    pub fn write(&mut self, dir: &Path) -> Result<()> {
117        let path = dir.join(LOCK_FILENAME);
118        if self.is_empty() {
119            if path.exists() {
120                std::fs::remove_file(&path).map_err(|err| crate::errors::failed_to_write_lock(path.display(), err))?;
121            }
122            return Ok(());
123        }
124        self.git.sort_by(|a, b| (&a.name, &a.git, &a.reference).cmp(&(&b.name, &b.git, &b.reference)));
125
126        let mut contents = serde_json::to_string_pretty(self)
127            .map_err(|err| crate::errors::failed_to_serialize_lock(path.display(), err))?;
128        contents.push('\n');
129        std::fs::write(&path, contents).map_err(|err| crate::errors::failed_to_write_lock(path.display(), err))?;
130        Ok(())
131    }
132
133    /// Whether any git dependency is recorded.
134    pub fn is_empty(&self) -> bool {
135        self.git.is_empty()
136    }
137}