1use chrono::{DateTime, Utc};
86use ggen_utils::error::{Error, Result};
87use lru::LruCache;
88use rayon::prelude::*;
89use serde::{Deserialize, Serialize};
90use std::collections::BTreeMap;
91use std::fs;
92use std::num::NonZeroUsize;
93use std::path::{Path, PathBuf};
94use std::sync::{Arc, Mutex};
95
96type DepCache = Arc<Mutex<LruCache<String, Option<Vec<String>>>>>;
100
101#[derive(Debug, Clone)]
103pub struct LockfileManager {
104 lockfile_path: PathBuf,
105 dep_cache: DepCache,
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct Lockfile {
112 pub version: String,
113 pub generated: DateTime<Utc>,
114 pub packs: Vec<LockEntry>,
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct LockEntry {
120 pub id: String,
121 pub version: String,
122 pub sha256: String,
123 pub source: String,
124 pub dependencies: Option<Vec<String>>,
125 #[serde(skip_serializing_if = "Option::is_none")]
127 pub pqc_signature: Option<String>,
128 #[serde(skip_serializing_if = "Option::is_none")]
130 pub pqc_pubkey: Option<String>,
131}
132
133impl LockfileManager {
134 pub fn new(project_dir: &Path) -> Self {
146 let lockfile_path = project_dir.join("ggen.lock");
147 let cache_size = NonZeroUsize::new(1000).expect("1000 is non-zero");
150 let dep_cache = Arc::new(Mutex::new(LruCache::new(cache_size)));
151 Self {
152 lockfile_path,
153 dep_cache,
154 }
155 }
156
157 pub fn with_path(lockfile_path: PathBuf) -> Self {
159 let cache_size = NonZeroUsize::new(1000).expect("1000 is non-zero");
161 let dep_cache = Arc::new(Mutex::new(LruCache::new(cache_size)));
162 Self {
163 lockfile_path,
164 dep_cache,
165 }
166 }
167
168 pub fn lockfile_path(&self) -> &Path {
181 &self.lockfile_path
182 }
183
184 pub fn load(&self) -> Result<Option<Lockfile>> {
186 if !self.lockfile_path.exists() {
187 return Ok(None);
188 }
189
190 let content = fs::read_to_string(&self.lockfile_path)
191 .map_err(|e| Error::with_context("Failed to read lockfile", &e.to_string()))?;
192
193 let lockfile: Lockfile = toml::from_str(&content)
194 .map_err(|e| Error::with_context("Failed to parse lockfile", &e.to_string()))?;
195
196 Ok(Some(lockfile))
197 }
198
199 pub fn create(&self) -> Result<Lockfile> {
201 Ok(Lockfile {
202 version: "1.0".to_string(),
203 generated: Utc::now(),
204 packs: Vec::new(),
205 })
206 }
207
208 pub fn save(&self, lockfile: &Lockfile) -> Result<()> {
210 if let Some(parent) = self.lockfile_path.parent() {
212 fs::create_dir_all(parent).map_err(|e| {
213 Error::with_context("Failed to create lockfile directory", &e.to_string())
214 })?;
215 }
216
217 let content = toml::to_string_pretty(lockfile)
218 .map_err(|e| Error::with_context("Failed to serialize lockfile", &e.to_string()))?;
219
220 fs::write(&self.lockfile_path, content)
221 .map_err(|e| Error::with_context("Failed to write lockfile", &e.to_string()))?;
222
223 Ok(())
224 }
225
226 pub fn upsert(&self, pack_id: &str, version: &str, sha256: &str, source: &str) -> Result<()> {
228 self.upsert_with_pqc(pack_id, version, sha256, source, None, None)
229 }
230
231 pub fn upsert_with_pqc(
233 &self, pack_id: &str, version: &str, sha256: &str, source: &str,
234 pqc_signature: Option<String>, pqc_pubkey: Option<String>,
235 ) -> Result<()> {
236 let mut lockfile = match self.load()? {
237 Some(lockfile) => lockfile,
238 None => self.create()?,
239 };
240
241 lockfile.packs.retain(|entry| entry.id != pack_id);
243
244 let dependencies = self.resolve_dependencies(pack_id, version, source)?;
246
247 lockfile.packs.push(LockEntry {
249 id: pack_id.to_string(),
250 version: version.to_string(),
251 sha256: sha256.to_string(),
252 source: source.to_string(),
253 dependencies,
254 pqc_signature,
255 pqc_pubkey,
256 });
257
258 lockfile.packs.sort_by(|a, b| a.id.cmp(&b.id));
260
261 self.save(&lockfile)
262 }
263
264 fn resolve_dependencies(
267 &self, pack_id: &str, version: &str, source: &str,
268 ) -> Result<Option<Vec<String>>> {
269 let cache_key = format!("{}@{}", pack_id, version);
270
271 {
273 let mut cache = self
274 .dep_cache
275 .lock()
276 .map_err(|e| Error::new(&format!("Dependency cache lock poisoned: {}", e)))?;
277 if let Some(cached_deps) = cache.get(&cache_key) {
278 return Ok(cached_deps.clone());
279 }
280 }
281
282 let result = if let Ok(manifest) = self.load_pack_manifest(pack_id, version, source) {
284 if !manifest.dependencies.is_empty() {
285 let resolved_deps: Vec<_> = manifest
288 .dependencies
289 .par_iter()
290 .map(|(dep_id, dep_version)| format!("{}@{}", dep_id, dep_version))
291 .collect();
292
293 let mut sorted_deps = resolved_deps;
295 sorted_deps.sort();
296
297 Some(sorted_deps)
298 } else {
299 None
300 }
301 } else {
302 None
303 };
304
305 {
307 let mut cache = self
308 .dep_cache
309 .lock()
310 .map_err(|e| Error::new(&format!("Dependency cache lock poisoned: {}", e)))?;
311 cache.put(cache_key, result.clone());
312 }
313
314 Ok(result)
315 }
316
317 fn load_pack_manifest(
319 &self, pack_id: &str, version: &str, _source: &str,
320 ) -> Result<crate::gpack::GpackManifest> {
321 if let Ok(cache_manager) = crate::cache::CacheManager::new() {
323 if let Ok(cached_pack) = cache_manager.load_cached(pack_id, version) {
324 if let Some(manifest) = cached_pack.manifest {
325 return Ok(manifest);
326 }
327 }
328 }
329
330 Err(Error::new(&format!(
333 "Could not load manifest for pack {}@{}",
334 pack_id, version
335 )))
336 }
337
338 pub fn remove(&self, pack_id: &str) -> Result<bool> {
340 let mut lockfile = match self.load()? {
341 Some(lockfile) => lockfile,
342 None => return Ok(false),
343 };
344
345 let original_len = lockfile.packs.len();
346 lockfile.packs.retain(|entry| entry.id != pack_id);
347
348 if lockfile.packs.len() < original_len {
349 self.save(&lockfile)?;
350 Ok(true)
351 } else {
352 Ok(false)
353 }
354 }
355
356 pub fn get(&self, pack_id: &str) -> Result<Option<LockEntry>> {
358 let lockfile = match self.load()? {
359 Some(lockfile) => lockfile,
360 None => return Ok(None),
361 };
362
363 Ok(lockfile.packs.into_iter().find(|entry| entry.id == pack_id))
364 }
365
366 pub fn list(&self) -> Result<Vec<LockEntry>> {
368 let lockfile = match self.load()? {
369 Some(lockfile) => lockfile,
370 None => return Ok(Vec::new()),
371 };
372
373 Ok(lockfile.packs)
374 }
375
376 pub fn is_installed(&self, pack_id: &str) -> Result<bool> {
378 Ok(self.get(pack_id)?.is_some())
379 }
380
381 pub fn installed_packs(&self) -> Result<BTreeMap<String, LockEntry>> {
384 let lockfile = match self.load()? {
385 Some(lockfile) => lockfile,
386 None => return Ok(BTreeMap::new()),
387 };
388
389 Ok(lockfile
390 .packs
391 .into_iter()
392 .map(|entry| (entry.id.clone(), entry))
393 .collect())
394 }
395
396 pub fn touch(&self) -> Result<()> {
398 let mut lockfile = match self.load()? {
399 Some(lockfile) => lockfile,
400 None => self.create()?,
401 };
402
403 lockfile.generated = Utc::now();
404 self.save(&lockfile)
405 }
406
407 pub fn stats(&self) -> Result<LockfileStats> {
409 let lockfile = match self.load()? {
410 Some(lockfile) => lockfile,
411 None => {
412 return Ok(LockfileStats {
413 total_packs: 0,
414 generated: None,
415 version: None,
416 })
417 }
418 };
419
420 Ok(LockfileStats {
421 total_packs: lockfile.packs.len(),
422 generated: Some(lockfile.generated),
423 version: Some(lockfile.version),
424 })
425 }
426
427 pub fn upsert_bulk(&self, packs: &[(String, String, String, String)]) -> Result<()> {
430 if packs.len() == 1 {
432 let (pack_id, version, sha256, source) = &packs[0];
433 return self.upsert(pack_id, version, sha256, source);
434 }
435
436 let mut lockfile = match self.load()? {
437 Some(lockfile) => lockfile,
438 None => self.create()?,
439 };
440
441 let entries: Result<Vec<_>> = packs
443 .par_iter()
444 .map(|(pack_id, version, sha256, source)| {
445 let dependencies = self.resolve_dependencies(pack_id, version, source)?;
447
448 Ok(LockEntry {
449 id: pack_id.clone(),
450 version: version.clone(),
451 sha256: sha256.clone(),
452 source: source.clone(),
453 dependencies,
454 pqc_signature: None,
455 pqc_pubkey: None,
456 })
457 })
458 .collect();
459
460 let new_entries = entries?;
461
462 let pack_ids: std::collections::BTreeSet<_> =
465 packs.iter().map(|(id, _, _, _)| id.as_str()).collect();
466 lockfile
467 .packs
468 .retain(|entry| !pack_ids.contains(entry.id.as_str()));
469
470 lockfile.packs.extend(new_entries);
472
473 lockfile.packs.sort_by(|a, b| a.id.cmp(&b.id));
475
476 self.save(&lockfile)
477 }
478
479 pub fn clear_cache(&self) {
481 if let Ok(mut cache) = self.dep_cache.lock() {
484 cache.clear();
485 }
486 }
487
488 pub fn cache_stats(&self) -> (usize, usize) {
490 self.dep_cache
492 .lock()
493 .map(|cache| (cache.len(), cache.cap().get()))
494 .unwrap_or((0, 0))
495 }
496}
497
498#[derive(Debug, Clone)]
500pub struct LockfileStats {
501 pub total_packs: usize,
502 pub generated: Option<DateTime<Utc>>,
503 pub version: Option<String>,
504}
505
506#[cfg(test)]
507mod tests {
508 use super::*;
509 use tempfile::TempDir;
510
511 #[test]
512 fn test_lockfile_manager_creation() {
513 let temp_dir = TempDir::new().unwrap();
514 let manager = LockfileManager::new(temp_dir.path());
515
516 assert_eq!(manager.lockfile_path(), temp_dir.path().join("ggen.lock"));
517 }
518
519 #[test]
520 fn test_lockfile_create_and_save() {
521 let temp_dir = TempDir::new().unwrap();
522 let manager = LockfileManager::new(temp_dir.path());
523
524 let lockfile = manager.create().unwrap();
525 manager.save(&lockfile).unwrap();
526
527 assert!(manager.lockfile_path().exists());
528 }
529
530 #[test]
531 fn test_lockfile_load_nonexistent() {
532 let temp_dir = TempDir::new().unwrap();
533 let manager = LockfileManager::new(temp_dir.path());
534
535 let loaded = manager.load().unwrap();
536 assert!(loaded.is_none());
537 }
538
539 #[test]
540 fn test_lockfile_upsert_and_get() {
541 let temp_dir = TempDir::new().unwrap();
542 let manager = LockfileManager::new(temp_dir.path());
543
544 manager
546 .upsert("io.ggen.test", "1.0.0", "abc123", "https://example.com")
547 .unwrap();
548
549 let entry = manager.get("io.ggen.test").unwrap().unwrap();
551 assert_eq!(entry.id, "io.ggen.test");
552 assert_eq!(entry.version, "1.0.0");
553 assert_eq!(entry.sha256, "abc123");
554 assert_eq!(entry.source, "https://example.com");
555 assert!(entry.pqc_signature.is_none());
556 assert!(entry.pqc_pubkey.is_none());
557 }
558
559 #[test]
560 fn test_lockfile_upsert_with_pqc() {
561 let temp_dir = TempDir::new().unwrap();
562 let manager = LockfileManager::new(temp_dir.path());
563
564 manager
566 .upsert_with_pqc(
567 "io.ggen.test",
568 "1.0.0",
569 "abc123",
570 "https://example.com",
571 Some("pqc_sig_base64".to_string()),
572 Some("pqc_pubkey_base64".to_string()),
573 )
574 .unwrap();
575
576 let entry = manager.get("io.ggen.test").unwrap().unwrap();
578 assert_eq!(entry.id, "io.ggen.test");
579 assert_eq!(entry.pqc_signature, Some("pqc_sig_base64".to_string()));
580 assert_eq!(entry.pqc_pubkey, Some("pqc_pubkey_base64".to_string()));
581 }
582
583 #[test]
584 fn test_lockfile_remove() {
585 let temp_dir = TempDir::new().unwrap();
586 let manager = LockfileManager::new(temp_dir.path());
587
588 manager
590 .upsert("io.ggen.test", "1.0.0", "abc123", "https://example.com")
591 .unwrap();
592 assert!(manager.is_installed("io.ggen.test").unwrap());
593
594 let removed = manager.remove("io.ggen.test").unwrap();
596 assert!(removed);
597 assert!(!manager.is_installed("io.ggen.test").unwrap());
598 }
599
600 #[test]
601 fn test_lockfile_stats() {
602 let temp_dir = TempDir::new().unwrap();
603 let manager = LockfileManager::new(temp_dir.path());
604
605 let stats = manager.stats().unwrap();
607 assert_eq!(stats.total_packs, 0);
608 assert!(stats.generated.is_none());
609 assert!(stats.version.is_none());
610
611 manager
613 .upsert("io.ggen.test", "1.0.0", "abc123", "https://example.com")
614 .unwrap();
615
616 let stats = manager.stats().unwrap();
617 assert_eq!(stats.total_packs, 1);
618 assert!(stats.generated.is_some());
619 assert_eq!(stats.version, Some("1.0".to_string()));
620 }
621}