1use crate::types;
2use anyhow::{Result, anyhow};
3use std::fs;
4use std::path::{Path, PathBuf};
5use std::sync::{LazyLock, Mutex};
6
7static RECORD_MUTEX: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
8
9fn get_lockfile_path(scope: types::Scope) -> Result<PathBuf> {
10 let path = if scope == types::Scope::Project {
11 std::env::current_dir()?.join("zoi.lock")
12 } else {
13 let home_dir = crate::utils::get_user_home()
14 .ok_or_else(|| anyhow!("Could not find home directory."))?;
15 home_dir.join(".zoi").join("pkgs").join("zoi.lock")
16 };
17
18 if let Some(parent) = path.parent() {
19 fs::create_dir_all(parent)?;
20 }
21 Ok(path)
22}
23
24fn read_lockfile(scope: types::Scope) -> Result<types::ZoiLockV2> {
25 let path = get_lockfile_path(scope)?;
26 if !path.exists() || fs::read_to_string(&path)?.trim().is_empty() {
27 return Ok(types::ZoiLockV2 {
28 version: "2".to_string(),
29 ..Default::default()
30 });
31 }
32 let content = fs::read_to_string(path)?;
33 let lockfile = serde_json::from_str(&content)?;
34 Ok(lockfile)
35}
36
37fn write_lockfile(lockfile: &mut types::ZoiLockV2, scope: types::Scope) -> Result<()> {
48 if crate::frozen::is_frozen() {
49 return Ok(());
50 }
51 let path = get_lockfile_path(scope)?;
52
53 if let Ok(store_dir) = crate::utils::get_store_base_dir(scope) {
54 lockfile.packages_hash = Some(format!(
55 "sha512-{}",
56 crate::hash::calculate_dir_hash(&store_dir).unwrap_or_default()
57 ));
58 }
59
60 let db_dir = if scope == types::Scope::Project {
61 std::env::current_dir()?
62 .join(".zoi")
63 .join("pkgs")
64 .join("db")
65 } else {
66 crate::utils::get_db_root().unwrap_or_default()
67 };
68
69 if db_dir.exists() {
70 lockfile.registries_hash = Some(format!(
71 "sha512-{}",
72 crate::hash::calculate_dir_hash(&db_dir).unwrap_or_default()
73 ));
74 }
75
76 let content = serde_json::to_string_pretty(lockfile)?;
77 fs::write(path, content)?;
78 Ok(())
79}
80
81pub fn record_package(
82 pkg: &types::Package,
83 reason: &types::InstallReason,
84 _installed_dependencies: &[String],
85 registry_handle: &str,
86 repo_type: &str,
87 _chosen_options: &[String],
88 _chosen_optionals: &[String],
89 sub_package: Option<String>,
90) -> Result<()> {
91 let _lock = RECORD_MUTEX
92 .lock()
93 .map_err(|e| anyhow!("Mutex poisoned: {}", e))?;
94 let mut lockfile = read_lockfile(pkg.scope)?;
95
96 let package_key = if let Some(sub) = &sub_package {
97 format!("@{}/{}:{}", pkg.repo.trim(), pkg.name.trim(), sub.trim())
98 } else {
99 format!("@{}/{}", pkg.repo.trim(), pkg.name.trim())
100 };
101
102 let os = std::env::consts::OS;
103 let arch = match std::env::consts::ARCH {
104 "x86_64" => "amd64",
105 "aarch64" => "arm64",
106 other => other,
107 };
108 let platform = format!("{}-{}", os, arch);
109
110 let hash = compute_package_hash(pkg, registry_handle);
111
112 let detail = types::LockPackageDetailV2 {
113 name: pkg.name.clone(),
114 sub_package: sub_package.clone(),
115 repo: pkg.repo.clone(),
116 repo_type: repo_type.to_string(),
117 version: pkg.version.clone().unwrap_or_default(),
118 epoch: pkg.epoch,
119 revision: pkg.revision.clone(),
120 registry: registry_handle.to_string(),
121 why: match reason {
122 types::InstallReason::Direct => "direct".to_string(),
123 types::InstallReason::Dependency { .. } => "dependency".to_string(),
124 },
125 description: pkg.description.clone(),
126 package_type_install: format!("{:?}", pkg.package_type).to_lowercase(),
127 install_method: if pkg.types.contains(&"source".to_string())
128 && !pkg.types.contains(&"pre-compiled".to_string())
129 {
130 "source".to_string()
131 } else {
132 "pre-compiled".to_string()
133 },
134 installed_sub_packages: sub_package.clone().map(|s| vec![s]).unwrap_or_default(),
135 platform,
136 hash,
137 dependencies: pkg.dependencies.clone().map(types::to_dependencies_v2),
138 };
139
140 lockfile.installed_packages.insert(package_key, detail);
141 lockfile.version = "2".to_string();
142
143 if !lockfile.registries.contains_key(registry_handle)
144 && let Some(reg_info) = resolve_registry_info(registry_handle)
145 {
146 lockfile
147 .registries
148 .insert(registry_handle.to_string(), reg_info);
149 }
150
151 write_lockfile(&mut lockfile, pkg.scope)
152}
153
154fn compute_package_hash(pkg: &types::Package, registry_handle: &str) -> String {
159 let Some(version) = &pkg.version else {
160 return String::new();
161 };
162 let Ok(store_base) = crate::utils::get_store_base_dir(pkg.scope) else {
163 return String::new();
164 };
165 let package_id = crate::utils::generate_package_id(registry_handle, &pkg.repo, &pkg.name);
166 let package_dir_name = crate::utils::get_package_dir_name(&package_id, &pkg.name);
167 let version_dir = store_base.join(&package_dir_name).join(version);
168 if version_dir.exists() {
169 format!(
170 "sha512-{}",
171 crate::hash::calculate_dir_hash(&version_dir).unwrap_or_default()
172 )
173 } else {
174 String::new()
175 }
176}
177
178fn resolve_registry_info(registry_handle: &str) -> Option<types::LockRegistryV2> {
179 let Ok(config) = crate::config::read_config() else {
180 return None;
181 };
182 let reg = config
183 .default_registry
184 .as_ref()
185 .filter(|r| r.handle == registry_handle)
186 .or_else(|| {
187 config
188 .added_registries
189 .iter()
190 .find(|r| r.handle == registry_handle)
191 })?;
192
193 let db_root = crate::utils::get_db_root().ok()?;
194 let reg_path = db_root.join(registry_handle);
195 let revision = resolve_git_head(®_path).unwrap_or_else(|| "unknown".to_string());
196
197 Some(types::LockRegistryV2 {
198 url: reg.url.clone(),
199 revision,
200 })
201}
202
203fn resolve_git_head(repo_path: &Path) -> Option<String> {
204 let head_file = repo_path.join(".git").join("HEAD");
205 let content = fs::read_to_string(&head_file).ok()?;
206 let content = content.trim();
207 if let Some(ref_path) = content.strip_prefix("ref: ") {
208 let ref_file = repo_path.join(".git").join(ref_path);
209 fs::read_to_string(&ref_file)
210 .ok()
211 .map(|s| s.trim().to_string())
212 } else {
213 Some(content.to_string())
214 }
215}
216
217pub fn update_package_reason(
218 manifest: &types::InstallManifest,
219 new_reason: types::InstallReason,
220) -> Result<()> {
221 let _lock = RECORD_MUTEX
222 .lock()
223 .map_err(|e| anyhow!("Mutex poisoned: {}", e))?;
224 let mut lockfile = read_lockfile(manifest.scope)?;
225 let repo = manifest.repo.trim();
226 let name = manifest.name.trim();
227
228 let package_key = if let Some(sub) = &manifest.sub_package {
229 format!("@{}/{}:{}", repo, name, sub.trim())
230 } else {
231 format!("@{}/{}", repo, name)
232 };
233
234 if let Some(pkg) = lockfile.installed_packages.get_mut(&package_key) {
235 pkg.why = match new_reason {
236 types::InstallReason::Direct => "direct".to_string(),
237 types::InstallReason::Dependency { .. } => "dependency".to_string(),
238 };
239 lockfile.version = "2".to_string();
240 write_lockfile(&mut lockfile, manifest.scope)?;
241 Ok(())
242 } else {
243 Err(anyhow!("Package '{}' not found in record.", manifest.name))
244 }
245}
246
247pub fn remove_package_from_record(manifest: &types::InstallManifest) -> Result<()> {
248 let _lock = RECORD_MUTEX
249 .lock()
250 .map_err(|e| anyhow!("Mutex poisoned: {}", e))?;
251 let mut lockfile = read_lockfile(manifest.scope)?;
252 let repo = manifest.repo.trim();
253 let name = manifest.name.trim();
254
255 let package_key = if let Some(sub) = &manifest.sub_package {
256 format!("@{}/{}:{}", repo, name, sub.trim())
257 } else {
258 format!("@{}/{}", repo, name)
259 };
260
261 if lockfile.installed_packages.remove(&package_key).is_some() {
262 lockfile.version = "2".to_string();
263 write_lockfile(&mut lockfile, manifest.scope)?;
264 }
265
266 Ok(())
267}
268
269pub fn get_recorded_packages() -> Result<Vec<types::LockPackageDetailV2>> {
270 let mut all_packages = Vec::new();
271 for scope in [
272 types::Scope::User,
273 types::Scope::System,
274 types::Scope::Project,
275 ] {
276 if let Ok(lockfile) = read_lockfile(scope) {
277 all_packages.extend(lockfile.installed_packages.into_values());
278 }
279 }
280 Ok(all_packages)
281}