1use crate::errors::{CompileError, Result};
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::fs;
8use std::path::Path;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct Lockfile {
13 pub version: u32,
15
16 pub root: RootPackage,
18
19 pub packages: Vec<LockedPackage>,
21
22 pub metadata: LockfileMetadata,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct RootPackage {
29 pub name: String,
30 pub version: String,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct LockedPackage {
36 pub name: String,
37 pub version: String,
38 pub source: PackageSource,
39 pub dependencies: Vec<String>,
40 pub checksum: String,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
45#[serde(tag = "type")]
46pub enum PackageSource {
47 Registry {
48 url: String,
49 },
50 Git {
51 url: String,
52 #[serde(skip_serializing_if = "Option::is_none")]
53 branch: Option<String>,
54 #[serde(skip_serializing_if = "Option::is_none")]
55 tag: Option<String>,
56 #[serde(skip_serializing_if = "Option::is_none")]
57 rev: Option<String>,
58 },
59 Path {
60 path: String,
61 },
62 Local {
63 path: String,
64 },
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct LockfileMetadata {
70 pub created_at: String,
72
73 pub palladium_version: String,
75
76 pub platform: PlatformInfo,
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct PlatformInfo {
83 pub os: String,
84 pub arch: String,
85}
86
87impl Lockfile {
88 const CURRENT_VERSION: u32 = 1;
90
91 pub fn new(root_name: &str, root_version: &str) -> Self {
93 Self {
94 version: Self::CURRENT_VERSION,
95 root: RootPackage {
96 name: root_name.to_string(),
97 version: root_version.to_string(),
98 },
99 packages: Vec::new(),
100 metadata: LockfileMetadata {
101 created_at: chrono::Utc::now().to_rfc3339(),
102 palladium_version: env!("CARGO_PKG_VERSION").to_string(),
103 platform: PlatformInfo {
104 os: std::env::consts::OS.to_string(),
105 arch: std::env::consts::ARCH.to_string(),
106 },
107 },
108 }
109 }
110
111 pub fn load(path: &Path) -> Result<Self> {
113 let content = fs::read_to_string(path).map_err(CompileError::IoError)?;
114
115 let lockfile: Self = toml::from_str(&content)
117 .map_err(|e| CompileError::Generic(format!("Failed to parse lockfile: {}", e)))?;
118
119 if lockfile.version > Self::CURRENT_VERSION {
121 return Err(CompileError::Generic(format!(
122 "Lockfile version {} is newer than supported version {}",
123 lockfile.version,
124 Self::CURRENT_VERSION
125 )));
126 }
127
128 Ok(lockfile)
129 }
130
131 pub fn save(&self, path: &Path) -> Result<()> {
133 let content = toml::to_string_pretty(self)
134 .map_err(|e| CompileError::Generic(format!("Failed to serialize lockfile: {}", e)))?;
135
136 fs::write(path, content).map_err(CompileError::IoError)?;
137 Ok(())
138 }
139
140 pub fn add_package(&mut self, package: LockedPackage) {
142 self.packages.retain(|p| p.name != package.name);
144 self.packages.push(package);
145
146 self.packages.sort_by(|a, b| a.name.cmp(&b.name));
148 }
149
150 pub fn get_package(&self, name: &str) -> Option<&LockedPackage> {
152 self.packages.iter().find(|p| p.name == name)
153 }
154
155 pub fn is_up_to_date(&self, manifest: &super::PackageManifest) -> bool {
157 if self.root.name != manifest.name || self.root.version != manifest.version {
159 return false;
160 }
161
162 for dep_name in manifest.dependencies.keys() {
164 if let Some(locked) = self.get_package(dep_name) {
165 let _ = locked; } else {
168 return false;
169 }
170 }
171
172 true
173 }
174
175 pub fn dependency_tree(&self) -> String {
177 let mut tree = String::new();
178 tree.push_str(&format!("{} v{}\n", self.root.name, self.root.version));
179
180 let mut dep_map: HashMap<String, Vec<String>> = HashMap::new();
182 for package in &self.packages {
183 for dep in &package.dependencies {
184 dep_map.entry(self.root.name.clone()).or_default().push(dep.clone());
185 }
186 }
187
188 self.print_deps(&self.root.name, &dep_map, &mut tree, "", true);
190
191 tree
192 }
193
194 fn print_deps(
196 &self,
197 package: &str,
198 dep_map: &HashMap<String, Vec<String>>,
199 output: &mut String,
200 prefix: &str,
201 _is_last: bool,
202 ) {
203 if let Some(deps) = dep_map.get(package) {
204 for (i, dep) in deps.iter().enumerate() {
205 let is_last_dep = i == deps.len() - 1;
206 let connector = if is_last_dep { "└── " } else { "├── " };
207 let extension = if is_last_dep { " " } else { "│ " };
208
209 if let Some(locked) = self.get_package(dep) {
210 output.push_str(&format!(
211 "{}{}{} v{}\n",
212 prefix, connector, locked.name, locked.version
213 ));
214
215 let new_prefix = format!("{}{}", prefix, extension);
216 self.print_deps(&locked.name, dep_map, output, &new_prefix, is_last_dep);
217 }
218 }
219 }
220 }
221
222 pub fn verify_checksums(&self, cache_dir: &Path) -> Result<()> {
224 for package in &self.packages {
225 let package_dir = cache_dir.join(&package.name).join(&package.version);
226
227 if !package_dir.exists() {
228 return Err(CompileError::Generic(format!(
229 "Package {} v{} not found in cache",
230 package.name, package.version
231 )));
232 }
233
234 }
237
238 Ok(())
239 }
240}
241
242pub struct LockfileDiff {
244 pub added: Vec<LockedPackage>,
245 pub removed: Vec<LockedPackage>,
246 pub updated: Vec<(LockedPackage, LockedPackage)>, }
248
249impl LockfileDiff {
250 pub fn compute(old: &Lockfile, new: &Lockfile) -> Self {
252 let mut added = Vec::new();
253 let mut removed = Vec::new();
254 let mut updated = Vec::new();
255
256 for new_pkg in &new.packages {
258 if let Some(old_pkg) = old.packages.iter().find(|p| p.name == new_pkg.name) {
259 if old_pkg.version != new_pkg.version {
260 updated.push((old_pkg.clone(), new_pkg.clone()));
261 }
262 } else {
263 added.push(new_pkg.clone());
264 }
265 }
266
267 for old_pkg in &old.packages {
269 if !new.packages.iter().any(|p| p.name == old_pkg.name) {
270 removed.push(old_pkg.clone());
271 }
272 }
273
274 Self {
275 added,
276 removed,
277 updated,
278 }
279 }
280
281 pub fn display(&self) -> String {
283 let mut output = String::new();
284
285 if !self.added.is_empty() {
286 output.push_str("Added:\n");
287 for pkg in &self.added {
288 output.push_str(&format!(" + {} v{}\n", pkg.name, pkg.version));
289 }
290 }
291
292 if !self.removed.is_empty() {
293 output.push_str("Removed:\n");
294 for pkg in &self.removed {
295 output.push_str(&format!(" - {} v{}\n", pkg.name, pkg.version));
296 }
297 }
298
299 if !self.updated.is_empty() {
300 output.push_str("Updated:\n");
301 for (old, new) in &self.updated {
302 output.push_str(&format!(
303 " ~ {} v{} -> v{}\n",
304 old.name, old.version, new.version
305 ));
306 }
307 }
308
309 if output.is_empty() {
310 output.push_str("No changes\n");
311 }
312
313 output
314 }
315}
316
317#[cfg(test)]
318mod tests {
319 use super::*;
320 use tempfile::TempDir;
321
322 #[test]
323 fn test_lockfile_creation() {
324 let lockfile = Lockfile::new("test_package", "1.0.0");
325 assert_eq!(lockfile.version, Lockfile::CURRENT_VERSION);
326 assert_eq!(lockfile.root.name, "test_package");
327 assert_eq!(lockfile.root.version, "1.0.0");
328 assert!(lockfile.packages.is_empty());
329 }
330
331 #[test]
332 fn test_lockfile_save_load() {
333 let temp_dir = TempDir::new().unwrap();
334 let lockfile_path = temp_dir.path().join("package.lock");
335
336 let mut lockfile = Lockfile::new("test_package", "1.0.0");
337 lockfile.add_package(LockedPackage {
338 name: "http".to_string(),
339 version: "1.1.0".to_string(),
340 source: PackageSource::Registry {
341 url: "https://packages.palladium-lang.org".to_string(),
342 },
343 dependencies: vec![],
344 checksum: "abc123".to_string(),
345 });
346
347 lockfile.save(&lockfile_path).unwrap();
349 assert!(lockfile_path.exists());
350
351 let loaded = Lockfile::load(&lockfile_path).unwrap();
353 assert_eq!(loaded.root.name, "test_package");
354 assert_eq!(loaded.packages.len(), 1);
355 assert_eq!(loaded.packages[0].name, "http");
356 assert_eq!(loaded.packages[0].version, "1.1.0");
357 }
358
359 #[test]
360 fn test_lockfile_diff() {
361 let mut old = Lockfile::new("test", "1.0.0");
362 old.add_package(LockedPackage {
363 name: "http".to_string(),
364 version: "1.0.0".to_string(),
365 source: PackageSource::Registry {
366 url: "https://packages.palladium-lang.org".to_string(),
367 },
368 dependencies: vec![],
369 checksum: "old".to_string(),
370 });
371 old.add_package(LockedPackage {
372 name: "json".to_string(),
373 version: "1.0.0".to_string(),
374 source: PackageSource::Registry {
375 url: "https://packages.palladium-lang.org".to_string(),
376 },
377 dependencies: vec![],
378 checksum: "old".to_string(),
379 });
380
381 let mut new = Lockfile::new("test", "1.0.0");
382 new.add_package(LockedPackage {
383 name: "http".to_string(),
384 version: "1.1.0".to_string(), source: PackageSource::Registry {
386 url: "https://packages.palladium-lang.org".to_string(),
387 },
388 dependencies: vec![],
389 checksum: "new".to_string(),
390 });
391 new.add_package(LockedPackage {
392 name: "xml".to_string(), version: "2.0.0".to_string(),
394 source: PackageSource::Registry {
395 url: "https://packages.palladium-lang.org".to_string(),
396 },
397 dependencies: vec![],
398 checksum: "new".to_string(),
399 });
400 let diff = LockfileDiff::compute(&old, &new);
403 assert_eq!(diff.added.len(), 1);
404 assert_eq!(diff.added[0].name, "xml");
405 assert_eq!(diff.removed.len(), 1);
406 assert_eq!(diff.removed[0].name, "json");
407 assert_eq!(diff.updated.len(), 1);
408 assert_eq!(diff.updated[0].0.name, "http");
409 assert_eq!(diff.updated[0].0.version, "1.0.0");
410 assert_eq!(diff.updated[0].1.version, "1.1.0");
411 }
412}