winisland_plugin_api/packager/
mod.rs1pub mod manifest;
2pub mod packaging;
3pub mod signing;
4
5use ed25519_dalek::SigningKey;
6use manifest::PluginManifest;
7use signing::{hash_file, load_signing_key, load_signing_key_from_env, sign_payload};
8use std::path::{Path, PathBuf};
9
10pub struct PluginPackager {
26 name: String,
27 author: String,
28 version: String,
29 description: String,
30 github_link: String,
31 dll_name: String,
32 dll_path: Option<PathBuf>,
33 extra_dirs: Vec<String>,
34 signing_key: Option<SigningKey>,
35 output: Option<PathBuf>,
36}
37
38impl PluginPackager {
39 pub fn from_cargo() -> Result<Self, String> {
45 let cargo_toml_path = Path::new("Cargo.toml");
46 let contents = std::fs::read_to_string(cargo_toml_path).map_err(|e| {
47 format!(
48 "Cannot read Cargo.toml (run from the plugin project root): {}",
49 e
50 )
51 })?;
52
53 let value: toml::Value =
54 toml::from_str(&contents).map_err(|e| format!("Cannot parse Cargo.toml: {}", e))?;
55
56 let pkg = value
57 .get("package")
58 .ok_or_else(|| "Cargo.toml missing [package] section".to_string())?;
59
60 let name = pkg
61 .get("name")
62 .and_then(|v| v.as_str())
63 .ok_or_else(|| "Cargo.toml missing package.name".to_string())?
64 .to_string();
65
66 let version = pkg
67 .get("version")
68 .and_then(|v| v.as_str())
69 .unwrap_or("0.1.0")
70 .to_string();
71
72 let author = pkg
73 .get("authors")
74 .and_then(|v| v.as_array())
75 .and_then(|a| a.first())
76 .and_then(|v| v.as_str())
77 .unwrap_or("")
78 .to_string();
79
80 let description = pkg
81 .get("description")
82 .and_then(|v| v.as_str())
83 .unwrap_or("")
84 .to_string();
85
86 let dll_name = name.replace('-', "_");
87
88 Ok(Self {
89 name,
90 author,
91 version,
92 description,
93 github_link: String::new(),
94 dll_name,
95 dll_path: None,
96 extra_dirs: Vec::new(),
97 signing_key: None,
98 output: None,
99 })
100 }
101
102 pub fn new(name: &str) -> Self {
104 Self {
105 name: name.to_string(),
106 author: String::new(),
107 version: "0.1.0".to_string(),
108 description: String::new(),
109 github_link: String::new(),
110 dll_name: name.to_string().replace('-', "_"),
111 dll_path: None,
112 extra_dirs: Vec::new(),
113 signing_key: None,
114 output: None,
115 }
116 }
117
118 pub fn author(&mut self, v: &str) -> &mut Self {
120 self.author = v.to_string();
121 self
122 }
123
124 pub fn version(&mut self, v: &str) -> &mut Self {
126 self.version = v.to_string();
127 self
128 }
129
130 pub fn description(&mut self, v: &str) -> &mut Self {
132 self.description = v.to_string();
133 self
134 }
135
136 pub fn github_link(&mut self, v: &str) -> &mut Self {
138 self.github_link = v.to_string();
139 self
140 }
141
142 pub fn dll_name(&mut self, name: &str) -> &mut Self {
147 self.dll_name = name.to_string();
148 self
149 }
150
151 pub fn dll_path(&mut self, path: &str) -> &mut Self {
155 self.dll_path = Some(PathBuf::from(path));
156 self
157 }
158
159 pub fn include_dir(&mut self, dir: &str) -> &mut Self {
165 self.extra_dirs.push(dir.to_string());
166 self
167 }
168
169 pub fn signing_key_path(&mut self, path: &str) -> &mut Self {
171 match load_signing_key(Path::new(path)) {
172 Ok(key) => {
173 self.signing_key = Some(key);
174 }
175 Err(e) => {
176 log::warn!("Signing key not loaded: {}", e);
177 }
178 }
179 self
180 }
181
182 pub fn signing_key_env(&mut self, var: &str) -> &mut Self {
184 match load_signing_key_from_env(var) {
185 Ok(key) => {
186 self.signing_key = Some(key);
187 }
188 Err(e) => {
189 log::warn!("Signing key not loaded from env '{}': {}", var, e);
190 }
191 }
192 self
193 }
194
195 pub fn signing_key_bytes(&mut self, key_bytes: &[u8; 64]) -> &mut Self {
197 match SigningKey::from_keypair_bytes(key_bytes) {
198 Ok(key) => {
199 self.signing_key = Some(key);
200 }
201 Err(e) => {
202 log::warn!("Signing key not loaded from bytes: {}", e);
203 }
204 }
205 self
206 }
207
208 pub fn output(&mut self, path: &str) -> &mut Self {
212 self.output = Some(PathBuf::from(path));
213 self
214 }
215
216 pub fn build(&self) -> Result<PathBuf, String> {
227 log::info!("Building plugin '{}' in release mode...", self.name);
229 let status = std::process::Command::new("cargo")
230 .args(["build", "--release"])
231 .status()
232 .map_err(|e| format!("Failed to run cargo build: {}", e))?;
233
234 if !status.success() {
235 return Err("cargo build --release failed".to_string());
236 }
237
238 let dll_path = self.locate_dll()?;
240 let dll_dest_name = dll_path
241 .file_name()
242 .and_then(|n| n.to_str())
243 .unwrap_or("plugin.dll");
244
245 let staging = tempfile::tempdir().map_err(|e| format!("Cannot create temp dir: {}", e))?;
247 let staging_path = staging.path();
248
249 std::fs::copy(&dll_path, staging_path.join(dll_dest_name))
251 .map_err(|e| format!("Cannot copy DLL: {}", e))?;
252
253 for dir in &self.extra_dirs {
255 let src = Path::new(dir);
256 if src.exists() {
257 let dst = staging_path.join(dir);
258 copy_dir_all(src, &dst)?;
259 } else {
260 log::warn!("Extra directory '{}' not found, skipping", dir);
261 }
262 }
263
264 let mut dll_hashes = Vec::new();
266 let dll_hash = hash_file(&dll_path).map_err(|e| format!("Cannot hash DLL: {}", e))?;
267 dll_hashes.push(dll_hash);
268 for dir in &self.extra_dirs {
270 let dll_dir = staging_path.join(dir);
271 if dll_dir.exists() {
272 collect_dll_hashes(&dll_dir, &mut dll_hashes)?;
273 }
274 }
275
276 let mut manifest = PluginManifest {
278 name: self.name.clone(),
279 author: self.author.clone(),
280 version: self.version.clone(),
281 description: self.description.clone(),
282 github_link: self.github_link.clone(),
283 signature: None,
284 dll_hashes: Some(dll_hashes.clone()),
285 };
286
287 if let Some(key) = &self.signing_key {
289 let payload = manifest.signing_payload();
290 let sig = sign_payload(key, payload.as_bytes());
291 manifest.signature = Some(sig);
292 log::info!("Plugin signed");
293 } else {
294 log::info!("Plugin not signed (no signing key provided)");
295 }
296
297 manifest
299 .write_to_yaml(&staging_path.join("plugin.yml"))
300 .map_err(|e| format!("Cannot write plugin.yml: {}", e))?;
301
302 let output_path = self
304 .output
305 .clone()
306 .unwrap_or_else(|| PathBuf::from(format!("target/{}-{}.zip", self.name, self.version)));
307
308 packaging::create_zip(staging_path, &output_path)?;
309
310 log::info!("Plugin packaged: {}", output_path.display());
311 Ok(output_path)
312 }
313
314 fn locate_dll(&self) -> Result<PathBuf, String> {
315 if let Some(path) = &self.dll_path {
316 if path.exists() {
317 return Ok(path.clone());
318 }
319 return Err(format!(
320 "Specified DLL path does not exist: {}",
321 path.display()
322 ));
323 }
324
325 let release_path = PathBuf::from(format!("target/release/{}.dll", self.dll_name));
327 if release_path.exists() {
328 return Ok(release_path);
329 }
330
331 let release_so = PathBuf::from(format!("target/release/lib{}.so", self.dll_name));
333 if release_so.exists() {
334 return Ok(release_so);
335 }
336
337 Err(format!(
338 "Cannot find built DLL. Expected at '{}' or '{}'. \
339 Make sure 'cargo build --release' completed successfully.",
340 release_path.display(),
341 release_so.display(),
342 ))
343 }
344}
345
346fn copy_dir_all(src: &Path, dst: &Path) -> Result<(), String> {
347 std::fs::create_dir_all(dst)
348 .map_err(|e| format!("Cannot create dir '{}': {}", dst.display(), e))?;
349
350 for entry in
351 std::fs::read_dir(src).map_err(|e| format!("Cannot read dir '{}': {}", src.display(), e))?
352 {
353 let entry = entry.map_err(|e| format!("Dir entry error: {}", e))?;
354 let ty = entry
355 .file_type()
356 .map_err(|e| format!("File type error: {}", e))?;
357 let src_path = entry.path();
358 let file_name = src_path
359 .file_name()
360 .ok_or_else(|| "Invalid filename".to_string())?;
361 let dst_path = dst.join(file_name);
362
363 if ty.is_dir() {
364 copy_dir_all(&src_path, &dst_path)?;
365 } else {
366 std::fs::copy(&src_path, &dst_path)
367 .map_err(|e| format!("Cannot copy '{}': {}", src_path.display(), e))?;
368 }
369 }
370 Ok(())
371}
372
373fn collect_dll_hashes(dir: &Path, hashes: &mut Vec<String>) -> Result<(), String> {
374 for entry in std::fs::read_dir(dir).map_err(|e| format!("Cannot read dir: {}", e))? {
375 let entry = entry.map_err(|e| format!("Dir entry error: {}", e))?;
376 let path = entry.path();
377 if path.extension().is_some_and(|ext| ext == "dll") {
378 let hash = hash_file(&path)?;
379 hashes.push(hash);
380 }
381 if entry.file_type().map(|t| t.is_dir()).unwrap_or(false) {
382 collect_dll_hashes(&path, hashes)?;
383 }
384 }
385 Ok(())
386}