maolan_engine/plugins/
mod.rs1pub mod clap_proc;
2pub mod ipc;
3#[cfg(all(unix, not(target_os = "macos")))]
4pub mod lv2_proc;
5pub mod types;
6pub mod vst3_proc;
7
8pub use types::*;
9
10use serde::de::DeserializeOwned;
11
12#[derive(serde::Deserialize)]
13struct ScanDiagnostic {
14 message: String,
15 plugin_uri: Option<String>,
16 plugin_name: Option<String>,
17 bundle_uri: Option<String>,
18}
19
20#[derive(serde::Deserialize)]
21struct ScanOutput<T> {
22 data: T,
23 errors: Vec<ScanDiagnostic>,
24 warnings: Vec<ScanDiagnostic>,
25}
26
27use crate::message::PluginKind;
28
29pub fn resolve_plugin_identifier(kind: PluginKind, identifier: &str) -> Result<String, String> {
30 if identifier.is_empty() {
31 return Err("plugin identifier is empty".to_string());
32 }
33 if identifier.contains('/')
34 || identifier.contains('\\')
35 || identifier.contains("::")
36 || identifier.contains('#')
37 || identifier.contains("://")
38 || identifier.starts_with("file:")
39 || std::path::Path::new(identifier).exists()
40 {
41 return Ok(identifier.to_string());
42 }
43
44 match kind {
45 PluginKind::Clap => {
46 let plugins = scan_plugins::<ClapPluginInfo>("clap")
47 .map_err(|e| format!("failed to scan CLAP plugins: {e}"))?;
48 plugins
49 .into_iter()
50 .find(|p| !p.id.is_empty() && p.id == identifier)
51 .map(|p| p.path)
52 .ok_or_else(|| format!("CLAP plugin ID not found: {identifier}"))
53 }
54 PluginKind::Vst3 => {
55 let plugins = scan_plugins::<Vst3PluginInfo>("vst3")
56 .map_err(|e| format!("failed to scan VST3 plugins: {e}"))?;
57 plugins
58 .into_iter()
59 .find(|p| !p.id.is_empty() && p.id == identifier)
60 .map(|p| p.path)
61 .ok_or_else(|| format!("VST3 plugin ID not found: {identifier}"))
62 }
63 PluginKind::Lv2 => {
64 let plugins = scan_plugins::<Lv2PluginInfo>("lv2")
65 .map_err(|e| format!("failed to scan LV2 plugins: {e}"))?;
66 plugins
67 .into_iter()
68 .find(|p| p.uri == identifier)
69 .map(|p| p.uri)
70 .ok_or_else(|| format!("LV2 plugin URI not found: {identifier}"))
71 }
72 }
73}
74
75pub fn scan_plugins<T: DeserializeOwned>(format: &str) -> Result<Vec<T>, String> {
76 let host_bin = ipc::find_plugin_host_binary().ok_or("maolan-plugin-host binary not found")?;
77
78 let mut cmd = std::process::Command::new(&host_bin);
79 cmd.arg("--scan")
80 .arg("--format")
81 .arg(format)
82 .arg("--path")
83 .arg("--system");
84 ipc::append_parent_log_level(&mut cmd);
85
86 let output = cmd
87 .output()
88 .map_err(|e| format!("failed to spawn plugin-host scanner: {e}"))?;
89
90 if !output.status.success() {
91 let stderr = String::from_utf8_lossy(&output.stderr);
92 return Err(format!(
93 "plugin-host scanner exited with code {:?}: {stderr}",
94 output.status.code()
95 ));
96 }
97
98 let json = String::from_utf8_lossy(&output.stdout);
99 let parsed: ScanOutput<Vec<T>> =
100 serde_json::from_str(&json).map_err(|e| format!("failed to parse scan JSON: {e}"))?;
101
102 for error in &parsed.errors {
103 tracing::error!(
104 message = %error.message,
105 plugin_uri = ?error.plugin_uri,
106 plugin_name = ?error.plugin_name,
107 bundle_uri = ?error.bundle_uri,
108 "plugin scan error"
109 );
110 }
111 for warning in &parsed.warnings {
112 tracing::warn!(
113 message = %warning.message,
114 plugin_uri = ?warning.plugin_uri,
115 plugin_name = ?warning.plugin_name,
116 bundle_uri = ?warning.bundle_uri,
117 "plugin scan warning"
118 );
119 }
120
121 Ok(parsed.data)
122}
123
124#[cfg(test)]
125mod tests {
126 use super::ScanOutput;
127
128 #[test]
129 fn scan_output_parses_wrapper() {
130 let json = r#"{
131 "errors": [
132 {
133 "message": "error: failed to open manifest.ttl",
134 "bundle_uri": "file:///tmp/broken.lv2/"
135 }
136 ],
137 "warnings": [
138 {
139 "message": "warning: duplicate version",
140 "plugin_uri": "http://example.com/plugin"
141 }
142 ],
143 "data": [{"name": "Test", "path": "/tmp/test.clap", "capabilities": null}]
144 }"#;
145 let output: ScanOutput<Vec<serde_json::Value>> = serde_json::from_str(json).unwrap();
146 assert_eq!(output.errors.len(), 1);
147 assert_eq!(
148 output.errors[0].message,
149 "error: failed to open manifest.ttl"
150 );
151 assert_eq!(
152 output.errors[0].bundle_uri,
153 Some("file:///tmp/broken.lv2/".to_string())
154 );
155 assert_eq!(output.warnings.len(), 1);
156 assert_eq!(
157 output.warnings[0].plugin_uri,
158 Some("http://example.com/plugin".to_string())
159 );
160 assert_eq!(output.data.len(), 1);
161 assert_eq!(output.data[0]["name"], "Test");
162 }
163}