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 #[cfg(all(unix, not(target_os = "macos")))]
64 PluginKind::Lv2 => {
65 let plugins = scan_plugins::<Lv2PluginInfo>("lv2")
66 .map_err(|e| format!("failed to scan LV2 plugins: {e}"))?;
67 plugins
68 .into_iter()
69 .find(|p| p.uri == identifier)
70 .map(|p| p.uri)
71 .ok_or_else(|| format!("LV2 plugin URI not found: {identifier}"))
72 }
73 }
74}
75
76pub fn scan_plugins<T: DeserializeOwned>(format: &str) -> Result<Vec<T>, String> {
77 let host_bin = ipc::find_plugin_host_binary().ok_or("maolan-plugin-host binary not found")?;
78
79 let mut cmd = std::process::Command::new(&host_bin);
80 cmd.arg("--scan")
81 .arg("--format")
82 .arg(format)
83 .arg("--path")
84 .arg("--system");
85 ipc::append_parent_log_level(&mut cmd);
86
87 let output = cmd
88 .output()
89 .map_err(|e| format!("failed to spawn plugin-host scanner: {e}"))?;
90
91 if !output.status.success() {
92 let stderr = String::from_utf8_lossy(&output.stderr);
93 return Err(format!(
94 "plugin-host scanner exited with code {:?}: {stderr}",
95 output.status.code()
96 ));
97 }
98
99 let json = String::from_utf8_lossy(&output.stdout);
100 let parsed: ScanOutput<Vec<T>> =
101 serde_json::from_str(&json).map_err(|e| format!("failed to parse scan JSON: {e}"))?;
102
103 for error in &parsed.errors {
104 tracing::error!(
105 message = %error.message,
106 plugin_uri = ?error.plugin_uri,
107 plugin_name = ?error.plugin_name,
108 bundle_uri = ?error.bundle_uri,
109 "plugin scan error"
110 );
111 }
112 for warning in &parsed.warnings {
113 tracing::warn!(
114 message = %warning.message,
115 plugin_uri = ?warning.plugin_uri,
116 plugin_name = ?warning.plugin_name,
117 bundle_uri = ?warning.bundle_uri,
118 "plugin scan warning"
119 );
120 }
121
122 Ok(parsed.data)
123}
124
125#[cfg(test)]
126mod tests {
127 use super::ScanOutput;
128
129 #[test]
130 fn scan_output_parses_wrapper() {
131 let json = r#"{
132 "errors": [
133 {
134 "message": "error: failed to open manifest.ttl",
135 "bundle_uri": "file:///tmp/broken.lv2/"
136 }
137 ],
138 "warnings": [
139 {
140 "message": "warning: duplicate version",
141 "plugin_uri": "http://example.com/plugin"
142 }
143 ],
144 "data": [{"name": "Test", "path": "/tmp/test.clap", "capabilities": null}]
145 }"#;
146 let output: ScanOutput<Vec<serde_json::Value>> = serde_json::from_str(json).unwrap();
147 assert_eq!(output.errors.len(), 1);
148 assert_eq!(
149 output.errors[0].message,
150 "error: failed to open manifest.ttl"
151 );
152 assert_eq!(
153 output.errors[0].bundle_uri,
154 Some("file:///tmp/broken.lv2/".to_string())
155 );
156 assert_eq!(output.warnings.len(), 1);
157 assert_eq!(
158 output.warnings[0].plugin_uri,
159 Some("http://example.com/plugin".to_string())
160 );
161 assert_eq!(output.data.len(), 1);
162 assert_eq!(output.data[0]["name"], "Test");
163 }
164}