1use crate::error::Result;
7use serde::{Deserialize, Serialize};
8use std::collections::BTreeMap;
9use std::path::Path;
10
11const LOCKFILE_VERSION: u32 = 1;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct Lockfile {
17 pub version: u32,
19 pub created_at: String,
21 pub platform: String,
23 pub packages: BTreeMap<String, LockedPackage>,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct LockedPackage {
30 pub version: String,
32 pub revision: u32,
34 pub bottle_sha256: Option<String>,
36 pub bottle_url: Option<String>,
38 pub source_sha256: Option<String>,
40 pub source_url: Option<String>,
42 #[serde(default)]
44 pub built_from_source: bool,
45 #[serde(default)]
47 pub dependencies: Vec<String>,
48}
49
50impl Lockfile {
51 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 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 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 pub fn add_package(&mut self, name: &str, package: LockedPackage) {
81 self.packages.insert(name.to_string(), package);
82 }
83
84 pub fn remove_package(&mut self, name: &str) {
86 self.packages.remove(name);
87 }
88
89 pub fn get_package(&self, name: &str) -> Option<&LockedPackage> {
91 self.packages.get(name)
92 }
93
94 pub fn is_locked(&self, name: &str) -> bool {
96 self.packages.contains_key(name)
97 }
98
99 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 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 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 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
159fn 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}