Skip to main content

zoi_resolver/
local.rs

1use crate::resolve::{PackageRequest, get_db_root};
2use anyhow::Result;
3use std::fs;
4use std::path::{Path, PathBuf};
5use walkdir::WalkDir;
6use zoi_core::config;
7use zoi_core::types::{self, InstallManifest, Scope};
8use zoi_core::utils;
9
10pub fn get_store_base_dir(scope: Scope) -> Result<PathBuf> {
11    utils::get_store_base_dir(scope)
12}
13
14pub fn get_package_dir(
15    scope: Scope,
16    registry_handle: &str,
17    repo_path: &str,
18    package_name: &str,
19) -> Result<PathBuf> {
20    let base_dir = get_store_base_dir(scope)?;
21    let package_id = utils::generate_package_id(registry_handle, repo_path, package_name);
22    let package_dir_name = utils::get_package_dir_name(&package_id, package_name);
23    Ok(base_dir.join(package_dir_name))
24}
25
26pub fn get_package_version_dir(
27    scope: Scope,
28    registry_handle: &str,
29    repo_path: &str,
30    package_name: &str,
31    version: &str,
32) -> Result<PathBuf> {
33    let package_dir = get_package_dir(scope, registry_handle, repo_path, package_name)?;
34    Ok(package_dir.join(version))
35}
36
37use rayon::prelude::*;
38
39pub fn get_installed_packages() -> Result<Vec<InstallManifest>> {
40    let scopes = [Scope::User, Scope::System, Scope::Project];
41
42    let installed: Vec<InstallManifest> = scopes
43        .into_par_iter()
44        .map(|scope| {
45            let mut manifests = Vec::new();
46            if let Ok(store_root) = get_store_base_dir(scope)
47                && store_root.exists()
48                && let Ok(entries) = fs::read_dir(store_root)
49            {
50                for entry in entries.flatten() {
51                    let path = entry.path();
52                    if !path.is_dir() {
53                        continue;
54                    }
55                    let latest_path = path.join("latest");
56                    if (latest_path.is_symlink() || latest_path.is_dir())
57                        && let Ok(sub_entries) = fs::read_dir(&latest_path)
58                    {
59                        for sub_entry in sub_entries.flatten() {
60                            let file_name = sub_entry.file_name().to_string_lossy().to_string();
61                            if file_name.starts_with("manifest") && file_name.ends_with(".yaml") {
62                                let manifest_path = sub_entry.path();
63                                if manifest_path.exists()
64                                    && let Ok(content) = fs::read_to_string(manifest_path)
65                                    && let Ok(manifest) =
66                                        serde_yaml::from_str::<InstallManifest>(&content)
67                                {
68                                    manifests.push(manifest);
69                                }
70                            }
71                        }
72                    }
73                }
74            }
75            manifests
76        })
77        .flatten()
78        .collect();
79
80    let mut sorted_installed = installed;
81    sorted_installed.sort_by(|a, b| a.name.cmp(&b.name));
82    Ok(sorted_installed)
83}
84
85#[derive(Debug)]
86pub struct InstalledPackage {
87    pub name: String,
88    pub sub_package: Option<String>,
89    pub version: String,
90    pub repo: String,
91    pub package_type: zoi_core::types::PackageType,
92}
93
94pub fn get_installed_packages_with_type() -> Result<Vec<InstalledPackage>> {
95    let manifests = get_installed_packages()?;
96    Ok(manifests
97        .into_iter()
98        .map(|m| InstalledPackage {
99            name: m.name,
100            sub_package: m.sub_package,
101            version: m.version,
102            repo: m.repo,
103            package_type: m.package_type,
104        })
105        .collect())
106}
107
108pub fn is_package_installed(
109    package_name: &str,
110    sub_package_name: Option<&str>,
111    scope: Scope,
112) -> Result<Option<InstallManifest>> {
113    let store_root = get_store_base_dir(scope)?;
114    if !store_root.exists() {
115        return Ok(None);
116    }
117
118    for entry in fs::read_dir(store_root)? {
119        let entry = entry?;
120        let path = entry.path();
121        if !path.is_dir() {
122            continue;
123        }
124
125        if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
126            let parts: Vec<&str> = file_name.splitn(2, '-').collect();
127            if parts.len() == 2 && parts[1] == package_name && parts[0].len() == 32 {
128                let latest_path = path.join("latest");
129                if (latest_path.is_symlink() || latest_path.is_dir())
130                    && let Ok(entries) = fs::read_dir(&latest_path)
131                {
132                    for entry in entries.filter_map(Result::ok) {
133                        let file_name = entry.file_name().to_string_lossy().to_string();
134                        if file_name.starts_with("manifest") && file_name.ends_with(".yaml") {
135                            let manifest_path = entry.path();
136                            if manifest_path.exists() {
137                                let content = fs::read_to_string(manifest_path)?;
138                                let manifest: InstallManifest = serde_yaml::from_str(&content)?;
139                                if manifest.name == package_name
140                                    && manifest.sub_package.as_deref() == sub_package_name
141                                {
142                                    return Ok(Some(manifest));
143                                }
144                            }
145                        }
146                    }
147                }
148            }
149        }
150    }
151
152    Ok(None)
153}
154
155pub fn get_installed_manifests_in_scope(scope: Scope) -> Result<Vec<InstallManifest>> {
156    let store_root = get_store_base_dir(scope)?;
157    if !store_root.exists() {
158        return Ok(Vec::new());
159    }
160
161    let mut manifests = Vec::new();
162    for entry in fs::read_dir(store_root)? {
163        let entry = entry?;
164        let path = entry.path();
165        if !path.is_dir() {
166            continue;
167        }
168
169        let latest_path = path.join("latest");
170        if !(latest_path.is_symlink() || latest_path.is_dir()) {
171            continue;
172        }
173
174        let Ok(entries) = fs::read_dir(&latest_path) else {
175            continue;
176        };
177
178        for entry in entries.filter_map(Result::ok) {
179            let file_name = entry.file_name().to_string_lossy().to_string();
180            if !file_name.starts_with("manifest") || !file_name.ends_with(".yaml") {
181                continue;
182            }
183
184            let manifest_path = entry.path();
185            if !manifest_path.exists() {
186                continue;
187            }
188
189            let content = fs::read_to_string(manifest_path)?;
190            let manifest: InstallManifest = serde_yaml::from_str(&content)?;
191            manifests.push(manifest);
192        }
193    }
194
195    Ok(manifests)
196}
197
198pub fn find_installed_manifests_matching(
199    request: &PackageRequest,
200    scope: Scope,
201) -> Result<Vec<InstallManifest>> {
202    let manifests = get_installed_manifests_in_scope(scope)?;
203    Ok(manifests
204        .into_iter()
205        .filter(|manifest| {
206            manifest.name == request.name
207                && manifest.sub_package == request.sub_package
208                && request
209                    .handle
210                    .as_ref()
211                    .is_none_or(|handle| manifest.registry_handle == *handle)
212                && request
213                    .repo
214                    .as_ref()
215                    .is_none_or(|repo| manifest.repo == *repo)
216                && request
217                    .version_spec
218                    .as_ref()
219                    .is_none_or(|version| manifest.version == *version)
220        })
221        .collect())
222}
223
224pub fn package_source_string(
225    registry_handle: &str,
226    repo: &str,
227    name: &str,
228    sub_package: Option<&str>,
229    version: &str,
230) -> String {
231    let registry_handle = registry_handle.trim();
232    let repo = repo.trim();
233    let name = name.trim();
234    let version = version.trim();
235
236    if let Some(sub_package) = sub_package {
237        format!(
238            "#{}@{}/{}:{}@{}",
239            registry_handle,
240            repo,
241            name,
242            sub_package.trim(),
243            version
244        )
245    } else {
246        format!("#{}@{}/{}@{}", registry_handle, repo, name, version)
247    }
248}
249
250pub fn installed_manifest_source(manifest: &InstallManifest) -> String {
251    package_source_string(
252        &manifest.registry_handle,
253        &manifest.repo,
254        &manifest.name,
255        manifest.sub_package.as_deref(),
256        &manifest.version,
257    )
258}
259
260pub fn get_packages_from_repos(repos: &[String]) -> Result<Vec<zoi_core::types::Package>> {
261    let db_root = get_db_root()?;
262    if !db_root.exists() {
263        return Err(anyhow::anyhow!(
264            "Package database not found. Please run 'zoi sync' first."
265        ));
266    }
267
268    let mut available = Vec::new();
269
270    for repo_name in repos {
271        let repo_path = db_root.join(repo_name);
272        if !repo_path.exists() {
273            continue;
274        }
275        for entry in WalkDir::new(repo_path).into_iter().filter_map(Result::ok) {
276            if !entry.file_type().is_dir() {
277                continue;
278            }
279
280            let pkg_name = entry.file_name().to_string_lossy();
281            let pkg_file_path = entry.path().join(format!("{}.pkg.lua", pkg_name));
282
283            if pkg_file_path.is_file() {
284                let pkg_file_path_str = pkg_file_path.to_str().ok_or_else(|| {
285                    anyhow::anyhow!("Package path contains invalid UTF-8: {:?}", pkg_file_path)
286                })?;
287                let mut pkg: zoi_core::types::Package =
288                    zoi_lua::parser::parse_lua_package(pkg_file_path_str, None, None, true)?;
289
290                if let Ok(repo_subpath) = entry.path().strip_prefix(&db_root) {
291                    let mut repo_path = repo_subpath
292                        .to_string_lossy()
293                        .to_string()
294                        .replace('\\', "/");
295                    let pkg_name_suffix = format!("/{}", pkg.name);
296                    if repo_path.ends_with(&pkg_name_suffix) {
297                        repo_path =
298                            repo_path[..repo_path.len() - pkg_name_suffix.len()].to_string();
299                    } else if repo_path == pkg.name {
300                        repo_path = String::new();
301                    }
302                    pkg.repo = repo_path;
303                }
304
305                available.push(pkg);
306            }
307        }
308    }
309
310    available.sort_by(|a, b| a.name.cmp(&b.name));
311    Ok(available)
312}
313
314pub fn get_all_available_packages() -> Result<Vec<zoi_core::types::Package>> {
315    let config = config::read_config()?;
316    if let Some(handle) = config
317        .default_registry
318        .as_ref()
319        .map(|r| &r.handle)
320        .filter(|h| !h.is_empty())
321    {
322        let repos_with_handle: Vec<String> = config
323            .repos
324            .iter()
325            .map(|repo| format!("{}/{}", handle, repo))
326            .collect();
327        get_packages_from_repos(&repos_with_handle)
328    } else {
329        Ok(Vec::new())
330    }
331}
332
333pub fn add_dependent(package_dir: &Path, dependent_id: &str) -> Result<()> {
334    let dependents_dir = package_dir.join("dependents");
335    fs::create_dir_all(&dependents_dir)?;
336    let dependent_file = dependents_dir.join(hex::encode(dependent_id));
337    fs::write(dependent_file, "")?;
338    Ok(())
339}
340
341pub fn remove_dependent(package_dir: &Path, dependent_id: &str) -> Result<()> {
342    let dependents_dir = package_dir.join("dependents");
343    if dependents_dir.exists() {
344        let dependent_file = dependents_dir.join(hex::encode(dependent_id));
345        if dependent_file.exists() {
346            fs::remove_file(dependent_file)?;
347        } else if let Some(pos) = dependent_id.rfind('@') {
348            let legacy_id = &dependent_id[..pos];
349            let legacy_file = dependents_dir.join(hex::encode(legacy_id));
350            if legacy_file.exists() {
351                fs::remove_file(legacy_file)?;
352            }
353        }
354    }
355    Ok(())
356}
357
358pub fn get_dependents(package_dir: &Path) -> Result<Vec<String>> {
359    let dependents_dir = package_dir.join("dependents");
360    let mut dependents = Vec::new();
361    if dependents_dir.exists() {
362        for entry in fs::read_dir(dependents_dir)? {
363            let entry = entry?;
364            let path = entry.path();
365            if path.is_file()
366                && let Some(file_name) = path.file_name().and_then(|s| s.to_str())
367                && let Ok(decoded) = hex::decode(file_name)
368                && let Ok(dependent_id) = String::from_utf8(decoded)
369            {
370                dependents.push(dependent_id);
371            }
372        }
373    }
374    Ok(dependents)
375}
376
377pub fn write_manifest(manifest: &InstallManifest) -> Result<()> {
378    let version_dir = get_package_version_dir(
379        manifest.scope,
380        &manifest.registry_handle,
381        &manifest.repo,
382        &manifest.name,
383        &manifest.version,
384    )?;
385    fs::create_dir_all(&version_dir)?;
386
387    let manifest_filename = if let Some(sub) = &manifest.sub_package {
388        format!("manifest-{}.yaml", sub)
389    } else {
390        "manifest.yaml".to_string()
391    };
392    let manifest_path = version_dir.join(manifest_filename);
393
394    let content = serde_yaml::to_string(&manifest)?;
395    fs::write(manifest_path, content)?;
396
397    let package_dir = get_package_dir(
398        manifest.scope,
399        &manifest.registry_handle,
400        &manifest.repo,
401        &manifest.name,
402    )?;
403    let latest_symlink_path = package_dir.join("latest");
404    zoi_core::utils::symlink_dir(&version_dir, &latest_symlink_path)?;
405
406    Ok(())
407}
408
409pub fn get_package_source_path(manifest: &InstallManifest) -> Result<PathBuf> {
410    let version_dir = get_package_version_dir(
411        manifest.scope,
412        &manifest.registry_handle,
413        &manifest.repo,
414        &manifest.name,
415        &manifest.version,
416    )?;
417    Ok(version_dir.join("package.pkg.lua"))
418}
419
420pub fn persist_package_source(manifest: &InstallManifest, source_path: &Path) -> Result<()> {
421    let stored_source_path = get_package_source_path(manifest)?;
422    if let Some(parent) = stored_source_path.parent() {
423        fs::create_dir_all(parent)?;
424    }
425    fs::copy(source_path, stored_source_path)?;
426    Ok(())
427}
428
429pub fn update_manifest_reason(
430    manifest: &InstallManifest,
431    new_reason: types::InstallReason,
432) -> Result<()> {
433    let mut updated_manifest = manifest.clone();
434    updated_manifest.reason = new_reason;
435    write_manifest(&updated_manifest)?;
436    Ok(())
437}