tinymist_query/
package.rs

1//! Package management tools.
2
3use std::path::PathBuf;
4
5use ecow::eco_format;
6// use reflexo_typst::typst::prelude::*;
7use serde::{Deserialize, Serialize};
8use tinymist_world::package::PackageSpec;
9use typst::World;
10use typst::diag::{EcoString, StrResult};
11use typst::syntax::package::PackageManifest;
12use typst::syntax::{FileId, VirtualPath};
13
14use crate::LocalContext;
15
16/// Information about a package.
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct PackageInfo {
19    /// The path to the package if any.
20    pub path: PathBuf,
21    /// The namespace the package lives in.
22    pub namespace: EcoString,
23    /// The name of the package within its namespace.
24    pub name: EcoString,
25    /// The package's version.
26    pub version: String,
27}
28
29impl From<(PathBuf, PackageSpec)> for PackageInfo {
30    fn from((path, spec): (PathBuf, PackageSpec)) -> Self {
31        Self {
32            path,
33            namespace: spec.namespace,
34            name: spec.name,
35            version: spec.version.to_string(),
36        }
37    }
38}
39
40/// Parses the manifest of the package located at `package_path`.
41pub fn get_manifest_id(spec: &PackageInfo) -> StrResult<FileId> {
42    Ok(FileId::new(
43        Some(PackageSpec {
44            namespace: spec.namespace.clone(),
45            name: spec.name.clone(),
46            version: spec.version.parse()?,
47        }),
48        VirtualPath::new("typst.toml"),
49    ))
50}
51
52/// Parses the manifest of the package located at `package_path`.
53pub fn get_manifest(world: &dyn World, toml_id: FileId) -> StrResult<PackageManifest> {
54    let toml_data = world
55        .file(toml_id)
56        .map_err(|err| eco_format!("failed to read package manifest ({})", err))?;
57
58    let string = std::str::from_utf8(&toml_data)
59        .map_err(|err| eco_format!("package manifest is not valid UTF-8 ({})", err))?;
60
61    toml::from_str(string)
62        .map_err(|err| eco_format!("package manifest is malformed ({})", err.message()))
63}
64
65/// Check Package.
66pub fn check_package(ctx: &mut LocalContext, spec: &PackageInfo) -> StrResult<()> {
67    let toml_id = get_manifest_id(spec)?;
68    let manifest = ctx.get_manifest(toml_id)?;
69
70    let entry_point = toml_id.join(&manifest.package.entrypoint);
71
72    ctx.shared_().preload_package(entry_point);
73    Ok(())
74}
75
76#[cfg(feature = "local-registry")]
77/// Get the packages in namespaces and their descriptions.
78pub fn list_package_by_namespace(
79    registry: &tinymist_world::package::registry::HttpRegistry,
80    ns: EcoString,
81) -> ecow::EcoVec<(PathBuf, PackageSpec)> {
82    use std::collections::HashSet;
83    use std::sync::OnceLock;
84
85    use ecow::eco_vec;
86    use parking_lot::Mutex;
87
88    trait IsDirFollowLinks {
89        fn is_dir_follow_links(&self) -> bool;
90    }
91
92    impl IsDirFollowLinks for PathBuf {
93        fn is_dir_follow_links(&self) -> bool {
94            // Although `canonicalize` is heavy, we must use it because `symlink_metadata`
95            // is not reliable.
96            self.canonicalize()
97                .map(|meta| meta.is_dir())
98                .unwrap_or(false)
99        }
100    }
101
102    fn once_log<T, E: std::fmt::Display>(result: Result<T, E>, site: &'static str) -> Option<T> {
103        let err = match result {
104            Ok(value) => return Some(value),
105            Err(err) => err,
106        };
107
108        static ONCE: OnceLock<Mutex<HashSet<&'static str>>> = OnceLock::new();
109        let mut once = ONCE.get_or_init(Default::default).lock();
110        if once.insert(site) {
111            log::error!("failed to perform {site}: {err}");
112        }
113
114        None
115    }
116
117    // search packages locally. We only search in the data
118    // directory and not the cache directory, because the latter is not
119    // intended for storage of local packages.
120    let mut packages = eco_vec![];
121
122    log::info!(
123        "searching for packages in namespace {ns} in paths {:?}",
124        registry.paths()
125    );
126    for dir in registry.paths() {
127        let local_path = dir.join(ns.as_str());
128        if !local_path.exists() || !local_path.is_dir_follow_links() {
129            continue;
130        }
131        // namespace/package_name/version
132        // 2. package_name
133        let Some(package_names) = once_log(std::fs::read_dir(local_path), "read local package")
134        else {
135            continue;
136        };
137        for package in package_names {
138            let Some(package) = once_log(package, "read package name") else {
139                continue;
140            };
141            if package.file_name().to_string_lossy().starts_with('.') {
142                continue;
143            }
144
145            let package_path = package.path();
146            if !package_path.is_dir_follow_links() {
147                continue;
148            }
149            // 3. version
150            let Some(versions) = once_log(std::fs::read_dir(package_path), "read package versions")
151            else {
152                continue;
153            };
154            for version in versions {
155                let Some(version) = once_log(version, "read package version") else {
156                    continue;
157                };
158                if version.file_name().to_string_lossy().starts_with('.') {
159                    continue;
160                }
161                let package_version_path = version.path();
162                if !package_version_path.is_dir_follow_links() {
163                    continue;
164                }
165                let Some(version) = once_log(
166                    version.file_name().to_string_lossy().parse(),
167                    "parse package version",
168                ) else {
169                    continue;
170                };
171                let spec = PackageSpec {
172                    namespace: ns.clone(),
173                    name: package.file_name().to_string_lossy().into(),
174                    version,
175                };
176                packages.push((package_version_path, spec));
177            }
178        }
179    }
180
181    packages
182}