Skip to main content

stout_state/
lockfile.rs

1//! Lockfile support for reproducible environments
2//!
3//! The lockfile (`stout.lock`) captures the exact versions of all packages
4//! installed, allowing for reproducible environments across machines.
5
6use crate::error::Result;
7use serde::{Deserialize, Serialize};
8use std::collections::BTreeMap;
9use std::path::Path;
10
11/// Lockfile format version
12const LOCKFILE_VERSION: u32 = 1;
13
14/// A lockfile for reproducible package installations
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct Lockfile {
17    /// Lockfile format version
18    pub version: u32,
19    /// When the lockfile was created
20    pub created_at: String,
21    /// Platform this lockfile was created on
22    pub platform: String,
23    /// Locked packages
24    pub packages: BTreeMap<String, LockedPackage>,
25}
26
27/// A locked package entry
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct LockedPackage {
30    /// Package version
31    pub version: String,
32    /// Package revision
33    pub revision: u32,
34    /// SHA256 of the bottle (if bottle install)
35    pub bottle_sha256: Option<String>,
36    /// Bottle URL (if bottle install)
37    pub bottle_url: Option<String>,
38    /// Source SHA256 (if built from source)
39    pub source_sha256: Option<String>,
40    /// Source URL (if built from source)
41    pub source_url: Option<String>,
42    /// Whether built from source
43    #[serde(default)]
44    pub built_from_source: bool,
45    /// Dependencies
46    #[serde(default)]
47    pub dependencies: Vec<String>,
48}
49
50impl Lockfile {
51    /// Create a new empty lockfile
52    pub fn new() -> Self {
53        let platform = format!("{}-{}", std::env::consts::OS, std::env::consts::ARCH);
54
55        let created_at = chrono_lite_timestamp();
56
57        Self {
58            version: LOCKFILE_VERSION,
59            created_at,
60            platform,
61            packages: BTreeMap::new(),
62        }
63    }
64
65    /// Load a lockfile from disk
66    pub fn load(path: impl AsRef<Path>) -> Result<Self> {
67        let contents = std::fs::read_to_string(path)?;
68        let lockfile: Lockfile = toml::from_str(&contents)?;
69        Ok(lockfile)
70    }
71
72    /// Save the lockfile to disk
73    pub fn save(&self, path: impl AsRef<Path>) -> Result<()> {
74        let contents = toml::to_string_pretty(self)?;
75        std::fs::write(path, contents)?;
76        Ok(())
77    }
78
79    /// Add or update a package
80    pub fn add_package(&mut self, name: &str, package: LockedPackage) {
81        self.packages.insert(name.to_string(), package);
82    }
83
84    /// Remove a package
85    pub fn remove_package(&mut self, name: &str) {
86        self.packages.remove(name);
87    }
88
89    /// Get a package
90    pub fn get_package(&self, name: &str) -> Option<&LockedPackage> {
91        self.packages.get(name)
92    }
93
94    /// Check if a package is locked
95    pub fn is_locked(&self, name: &str) -> bool {
96        self.packages.contains_key(name)
97    }
98
99    /// Check if the lockfile matches the current platform
100    pub fn matches_platform(&self) -> bool {
101        let current = format!("{}-{}", std::env::consts::OS, std::env::consts::ARCH);
102        self.platform == current
103    }
104
105    /// Get all package names
106    pub fn package_names(&self) -> impl Iterator<Item = &str> {
107        self.packages.keys().map(|s| s.as_str())
108    }
109}
110
111impl Default for Lockfile {
112    fn default() -> Self {
113        Self::new()
114    }
115}
116
117impl LockedPackage {
118    /// Create a new locked package from a bottle installation
119    pub fn from_bottle(
120        version: &str,
121        revision: u32,
122        bottle_url: &str,
123        bottle_sha256: &str,
124        dependencies: Vec<String>,
125    ) -> Self {
126        Self {
127            version: version.to_string(),
128            revision,
129            bottle_sha256: Some(bottle_sha256.to_string()),
130            bottle_url: Some(bottle_url.to_string()),
131            source_sha256: None,
132            source_url: None,
133            built_from_source: false,
134            dependencies,
135        }
136    }
137
138    /// Create a new locked package from a source build
139    pub fn from_source(
140        version: &str,
141        revision: u32,
142        source_url: &str,
143        source_sha256: &str,
144        dependencies: Vec<String>,
145    ) -> Self {
146        Self {
147            version: version.to_string(),
148            revision,
149            bottle_sha256: None,
150            bottle_url: None,
151            source_sha256: Some(source_sha256.to_string()),
152            source_url: Some(source_url.to_string()),
153            built_from_source: true,
154            dependencies,
155        }
156    }
157}
158
159/// Simple timestamp without external dependencies
160fn chrono_lite_timestamp() -> String {
161    use std::time::{SystemTime, UNIX_EPOCH};
162    let duration = SystemTime::now()
163        .duration_since(UNIX_EPOCH)
164        .unwrap_or_default();
165    format!("{}", duration.as_secs())
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    #[test]
173    fn test_lockfile_creation() {
174        let lockfile = Lockfile::new();
175        assert_eq!(lockfile.version, LOCKFILE_VERSION);
176        assert!(lockfile.packages.is_empty());
177    }
178
179    #[test]
180    fn test_add_package() {
181        let mut lockfile = Lockfile::new();
182        let pkg = LockedPackage::from_bottle(
183            "1.0.0",
184            0,
185            "https://example.com/pkg.tar.gz",
186            "abc123",
187            vec!["dep1".to_string()],
188        );
189        lockfile.add_package("test-pkg", pkg);
190        assert!(lockfile.is_locked("test-pkg"));
191    }
192}