Skip to main content

palladium/package/
lockfile.rs

1// Lockfile management for Palladium package manager
2// "Locking down the legendary dependencies"
3
4use crate::errors::{CompileError, Result};
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::fs;
8use std::path::Path;
9
10/// Lockfile format for reproducible builds
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct Lockfile {
13    /// Lockfile format version
14    pub version: u32,
15    
16    /// Root package information
17    pub root: RootPackage,
18    
19    /// Resolved packages
20    pub packages: Vec<LockedPackage>,
21    
22    /// Metadata
23    pub metadata: LockfileMetadata,
24}
25
26/// Root package information
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct RootPackage {
29    pub name: String,
30    pub version: String,
31}
32
33/// Locked package information
34#[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/// Package source information
44#[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/// Lockfile metadata
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct LockfileMetadata {
70    /// Timestamp of lockfile creation
71    pub created_at: String,
72    
73    /// Palladium version used to create the lockfile
74    pub palladium_version: String,
75    
76    /// Platform information
77    pub platform: PlatformInfo,
78}
79
80/// Platform information
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct PlatformInfo {
83    pub os: String,
84    pub arch: String,
85}
86
87impl Lockfile {
88    /// Current lockfile format version
89    const CURRENT_VERSION: u32 = 1;
90    
91    /// Create a new lockfile
92    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    /// Load lockfile from disk
112    pub fn load(path: &Path) -> Result<Self> {
113        let content = fs::read_to_string(path).map_err(CompileError::IoError)?;
114        
115        // Parse as TOML
116        let lockfile: Self = toml::from_str(&content)
117            .map_err(|e| CompileError::Generic(format!("Failed to parse lockfile: {}", e)))?;
118        
119        // Validate version
120        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    /// Save lockfile to disk
132    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    /// Add a locked package
141    pub fn add_package(&mut self, package: LockedPackage) {
142        // Remove existing version if any
143        self.packages.retain(|p| p.name != package.name);
144        self.packages.push(package);
145        
146        // Sort packages for deterministic output
147        self.packages.sort_by(|a, b| a.name.cmp(&b.name));
148    }
149    
150    /// Get a locked package by name
151    pub fn get_package(&self, name: &str) -> Option<&LockedPackage> {
152        self.packages.iter().find(|p| p.name == name)
153    }
154    
155    /// Check if lockfile is up to date with manifest
156    pub fn is_up_to_date(&self, manifest: &super::PackageManifest) -> bool {
157        // Check root package
158        if self.root.name != manifest.name || self.root.version != manifest.version {
159            return false;
160        }
161        
162        // Check if all manifest dependencies are in lockfile
163        for dep_name in manifest.dependencies.keys() {
164            if let Some(locked) = self.get_package(dep_name) {
165                // TODO: Check if locked version satisfies dependency spec
166                let _ = locked; // Placeholder
167            } else {
168                return false;
169            }
170        }
171        
172        true
173    }
174    
175    /// Generate a dependency tree for display
176    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        // Build dependency map
181        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        // Print tree
189        self.print_deps(&self.root.name, &dep_map, &mut tree, "", true);
190        
191        tree
192    }
193    
194    /// Recursively print dependencies
195    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    /// Verify checksums of all packages
223    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            // TODO: Actually compute and verify checksum
235            // For now, just check that the directory exists
236        }
237        
238        Ok(())
239    }
240}
241
242/// Lockfile diff for showing what changed
243pub struct LockfileDiff {
244    pub added: Vec<LockedPackage>,
245    pub removed: Vec<LockedPackage>,
246    pub updated: Vec<(LockedPackage, LockedPackage)>, // (old, new)
247}
248
249impl LockfileDiff {
250    /// Compute diff between two lockfiles
251    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        // Find added and updated packages
257        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        // Find removed packages
268        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    /// Display the diff in a human-readable format
282    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        // Save
348        lockfile.save(&lockfile_path).unwrap();
349        assert!(lockfile_path.exists());
350        
351        // Load
352        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(), // Updated
385            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(), // Added
393            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        // json removed
401        
402        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}