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