zsh/extensions/pkg/
manifest.rs1use serde::{Deserialize, Serialize};
26use std::path::Path;
27
28use super::{PkgError, PkgResult};
29
30pub const MANIFEST_FILE: &str = "znative.toml";
32
33#[derive(Debug, Clone, Serialize, Deserialize, Default)]
35pub struct PluginManifest {
36 #[serde(default)]
38 pub plugin: PluginMeta,
39 #[serde(default, skip_serializing_if = "Option::is_none")]
41 pub native: Option<NativeSpec>,
42 #[serde(default, skip_serializing_if = "Option::is_none")]
44 pub script: Option<ScriptSpec>,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize, Default)]
49pub struct PluginMeta {
50 #[serde(default, skip_serializing_if = "String::is_empty")]
52 pub name: String,
53 #[serde(default, skip_serializing_if = "String::is_empty")]
55 pub version: String,
56 #[serde(default, skip_serializing_if = "String::is_empty")]
58 pub description: String,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize, Default)]
63pub struct NativeSpec {
64 #[serde(default, skip_serializing_if = "String::is_empty")]
67 pub lib: String,
68 #[serde(default, skip_serializing_if = "Option::is_none")]
71 pub build: Option<bool>,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize, Default)]
76pub struct ScriptSpec {
77 #[serde(default, skip_serializing_if = "Vec::is_empty")]
79 pub source: Vec<String>,
80 #[serde(default, skip_serializing_if = "Vec::is_empty")]
82 pub fpath: Vec<String>,
83}
84
85#[derive(Debug, Clone)]
87pub enum PluginKind {
88 Native(NativeSpec),
90 Script(ScriptSpec),
92}
93
94impl PluginManifest {
95 #[allow(clippy::should_implement_trait)]
97 pub fn from_str(s: &str) -> PkgResult<PluginManifest> {
98 toml::from_str::<PluginManifest>(s)
99 .map_err(|e| PkgError::Manifest(format!("znative.toml: {}", e.message())))
100 }
101
102 pub fn load(dir: &Path) -> PkgResult<Option<PluginManifest>> {
104 let path = dir.join(MANIFEST_FILE);
105 if !path.is_file() {
106 return Ok(None);
107 }
108 let s = std::fs::read_to_string(&path)
109 .map_err(|e| PkgError::Io(format!("read {}: {}", path.display(), e)))?;
110 Ok(Some(PluginManifest::from_str(&s)?))
111 }
112}
113
114impl PluginKind {
115 pub fn detect(dir: &Path, manifest: Option<&PluginManifest>) -> PkgResult<PluginKind> {
126 if let Some(m) = manifest {
127 if let Some(n) = &m.native {
128 return Ok(PluginKind::Native(n.clone()));
129 }
130 if let Some(s) = &m.script {
131 return Ok(PluginKind::Script(s.clone()));
132 }
133 }
134 if has_cdylib(dir) || cargo_is_cdylib(dir) {
137 return Ok(PluginKind::Native(NativeSpec::default()));
138 }
139 let mut source: Vec<String> = Vec::new();
140 let mut fpath: Vec<String> = Vec::new();
141 for entry in std::fs::read_dir(dir)? {
142 let name = entry?.file_name().to_string_lossy().into_owned();
143 if name.ends_with(".plugin.zsh") {
144 source.push(name);
145 }
146 }
147 if dir.join("functions").is_dir() {
148 fpath.push("functions".into());
149 }
150 if source.is_empty() {
152 if let Some(base) = dir.file_name().and_then(|s| s.to_str()) {
153 let cand = format!("{}.zsh", base);
154 if dir.join(&cand).is_file() {
155 source.push(cand);
156 }
157 }
158 }
159 if source.is_empty() && fpath.is_empty() {
160 for entry in std::fs::read_dir(dir)? {
161 let name = entry?.file_name().to_string_lossy().into_owned();
162 if name.ends_with(".zsh") {
163 source.push(name);
164 }
165 }
166 }
167 if source.is_empty() && fpath.is_empty() {
168 return Err(PkgError::Unknown(
169 "could not determine plugin kind: no znative.toml, no *.plugin.zsh, \
170 no functions/, and no Rust cdylib/Cargo.toml"
171 .into(),
172 ));
173 }
174 source.sort();
175 Ok(PluginKind::Script(ScriptSpec { source, fpath }))
176 }
177}
178
179fn has_cdylib(dir: &Path) -> bool {
181 let Ok(rd) = std::fs::read_dir(dir) else {
182 return false;
183 };
184 for entry in rd.flatten() {
185 let n = entry.file_name();
186 let n = n.to_string_lossy();
187 if n.starts_with("lib") && (n.ends_with(".dylib") || n.ends_with(".so")) {
188 return true;
189 }
190 }
191 false
192}
193
194fn cargo_is_cdylib(dir: &Path) -> bool {
197 let cargo = dir.join("Cargo.toml");
198 let Ok(s) = std::fs::read_to_string(&cargo) else {
199 return false;
200 };
201 s.contains("cdylib")
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207
208 #[test]
209 fn parses_native_manifest() {
210 let m = PluginManifest::from_str(
211 "[plugin]\nname='x'\nversion='0.1.0'\n[native]\nlib='foo'\n",
212 )
213 .unwrap();
214 assert_eq!(m.plugin.name, "x");
215 assert_eq!(m.native.unwrap().lib, "foo");
216 }
217
218 #[test]
219 fn parses_script_manifest() {
220 let m = PluginManifest::from_str(
221 "[plugin]\nname='y'\n[script]\nsource=['y.plugin.zsh']\nfpath=['fns']\n",
222 )
223 .unwrap();
224 let s = m.script.unwrap();
225 assert_eq!(s.source, vec!["y.plugin.zsh"]);
226 assert_eq!(s.fpath, vec!["fns"]);
227 }
228}