1use anyhow::{Result, anyhow};
2use std::fs;
3use zoi_core::types;
4
5fn get_lockfile_path() -> Result<std::path::PathBuf> {
6 Ok(std::env::current_dir()?.join("zoi.lock"))
7}
8
9fn read_lockfile_from(path: &std::path::Path) -> Result<Option<types::ZoiLockV2>> {
10 if !path.exists() {
11 return Ok(None);
12 }
13 let content = fs::read_to_string(path)?;
14 if content.trim().is_empty() {
15 return Ok(None);
16 }
17 serde_json::from_str(&content).map(Some).map_err(|e| {
18 anyhow!(
19 "Failed to parse {}. It might be corrupted or in an old format. Error: {}",
20 path.display(),
21 e
22 )
23 })
24}
25
26fn is_lockfile_compatible(lockfile: &types::ZoiLockV2) -> bool {
27 let current_platform = zoi_core::utils::get_platform().unwrap_or_default();
28 if lockfile.installed_packages.is_empty() {
29 return true;
30 }
31 lockfile.installed_packages.values().all(|pkg| {
32 pkg.platform.is_empty()
33 || pkg.platform == current_platform
34 || zoi_core::utils::is_platform_compatible(
35 ¤t_platform,
36 std::slice::from_ref(&pkg.platform),
37 )
38 })
39}
40
41pub fn read_zoi_lock() -> Result<types::ZoiLockV2> {
42 let path = get_lockfile_path()?;
43
44 if let Some(lockfile) = read_lockfile_from(&path)? {
45 if is_lockfile_compatible(&lockfile) {
46 return Ok(lockfile);
47 }
48
49 let platform = zoi_core::utils::get_platform().unwrap_or_default();
50 let platform_path = path.with_file_name(format!("zoi.{}.lock", platform));
51 if let Some(platform_lock) = read_lockfile_from(&platform_path)? {
52 return Ok(platform_lock);
53 }
54
55 eprintln!(
56 "Warning: zoi.lock has packages targeting a different platform \
57 and no zoi.{}.lock was found, falling back to unconstrained resolution",
58 platform
59 );
60 }
61
62 Ok(types::ZoiLockV2 {
63 version: "2".to_string(),
64 ..Default::default()
65 })
66}
67
68pub fn write_zoi_lock(lockfile: &mut types::ZoiLockV2) -> Result<()> {
69 if zoi_core::frozen::is_frozen() {
70 return Ok(());
71 }
72 let path = get_lockfile_path()?;
73
74 if let Ok(store_dir) = zoi_core::utils::get_store_base_dir(types::Scope::Project) {
75 lockfile.packages_hash = Some(format!(
76 "sha512-{}",
77 zoi_core::hash::calculate_dir_hash(&store_dir).unwrap_or_default()
78 ));
79 }
80
81 let db_dir = std::env::current_dir()?
82 .join(".zoi")
83 .join("pkgs")
84 .join("db");
85
86 if db_dir.exists() {
87 lockfile.registries_hash = Some(format!(
88 "sha512-{}",
89 zoi_core::hash::calculate_dir_hash(&db_dir).unwrap_or_default()
90 ));
91 }
92
93 let content = serde_json::to_string_pretty(lockfile)?;
94 fs::write(path, content)?;
95 Ok(())
96}
97
98#[derive(Debug, Clone, PartialEq, Eq)]
99pub struct FrozenLockPackage {
100 pub source: String,
101 pub revision: String,
102 pub direct: bool,
103 pub chosen_options: Vec<String>,
104 pub chosen_optionals: Vec<String>,
105 pub dependencies: Option<types::DependenciesV2>,
106 pub git_sha: Option<String>,
107}
108
109pub fn locked_packages(lockfile: &types::ZoiLockV2) -> Vec<FrozenLockPackage> {
110 let mut packages = Vec::new();
111
112 for (key, detail) in &lockfile.installed_packages {
113 packages.push(FrozenLockPackage {
114 source: format!("{}@{}", key.trim(), detail.version),
115 revision: detail.revision.clone(),
116 direct: detail.why == "direct",
117 chosen_options: Vec::new(),
118 chosen_optionals: Vec::new(),
119 dependencies: detail.dependencies.clone(),
120 git_sha: None,
121 });
122 }
123
124 packages.sort_by(|a, b| a.source.cmp(&b.source));
125 packages
126}
127
128pub fn sources_from_lock(lockfile: &types::ZoiLockV2) -> Vec<String> {
129 locked_packages(lockfile)
130 .into_iter()
131 .map(|entry| entry.source)
132 .collect()
133}