pub mod manifest;
pub mod packaging;
pub mod signing;
use ed25519_dalek::SigningKey;
use manifest::PluginManifest;
use signing::{hash_file, load_signing_key, load_signing_key_from_env, sign_payload};
use std::path::{Path, PathBuf};
pub struct PluginPackager {
name: String,
author: String,
version: String,
description: String,
github_link: String,
dll_name: String,
dll_path: Option<PathBuf>,
extra_dirs: Vec<String>,
signing_key: Option<SigningKey>,
output: Option<PathBuf>,
}
impl PluginPackager {
pub fn from_cargo() -> Result<Self, String> {
let cargo_toml_path = Path::new("Cargo.toml");
let contents = std::fs::read_to_string(cargo_toml_path).map_err(|e| {
format!(
"Cannot read Cargo.toml (run from the plugin project root): {}",
e
)
})?;
let value: toml::Value =
toml::from_str(&contents).map_err(|e| format!("Cannot parse Cargo.toml: {}", e))?;
let pkg = value
.get("package")
.ok_or_else(|| "Cargo.toml missing [package] section".to_string())?;
let name = pkg
.get("name")
.and_then(|v| v.as_str())
.ok_or_else(|| "Cargo.toml missing package.name".to_string())?
.to_string();
let version = pkg
.get("version")
.and_then(|v| v.as_str())
.unwrap_or("0.1.0")
.to_string();
let author = pkg
.get("authors")
.and_then(|v| v.as_array())
.and_then(|a| a.first())
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let description = pkg
.get("description")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let dll_name = name.replace('-', "_");
Ok(Self {
name,
author,
version,
description,
github_link: String::new(),
dll_name,
dll_path: None,
extra_dirs: Vec::new(),
signing_key: None,
output: None,
})
}
pub fn new(name: &str) -> Self {
Self {
name: name.to_string(),
author: String::new(),
version: "0.1.0".to_string(),
description: String::new(),
github_link: String::new(),
dll_name: name.to_string().replace('-', "_"),
dll_path: None,
extra_dirs: Vec::new(),
signing_key: None,
output: None,
}
}
pub fn author(&mut self, v: &str) -> &mut Self {
self.author = v.to_string();
self
}
pub fn version(&mut self, v: &str) -> &mut Self {
self.version = v.to_string();
self
}
pub fn description(&mut self, v: &str) -> &mut Self {
self.description = v.to_string();
self
}
pub fn github_link(&mut self, v: &str) -> &mut Self {
self.github_link = v.to_string();
self
}
pub fn dll_name(&mut self, name: &str) -> &mut Self {
self.dll_name = name.to_string();
self
}
pub fn dll_path(&mut self, path: &str) -> &mut Self {
self.dll_path = Some(PathBuf::from(path));
self
}
pub fn include_dir(&mut self, dir: &str) -> &mut Self {
self.extra_dirs.push(dir.to_string());
self
}
pub fn signing_key_path(&mut self, path: &str) -> &mut Self {
match load_signing_key(Path::new(path)) {
Ok(key) => {
self.signing_key = Some(key);
}
Err(e) => {
log::warn!("Signing key not loaded: {}", e);
}
}
self
}
pub fn signing_key_env(&mut self, var: &str) -> &mut Self {
match load_signing_key_from_env(var) {
Ok(key) => {
self.signing_key = Some(key);
}
Err(e) => {
log::warn!("Signing key not loaded from env '{}': {}", var, e);
}
}
self
}
pub fn signing_key_bytes(&mut self, key_bytes: &[u8; 64]) -> &mut Self {
match SigningKey::from_keypair_bytes(key_bytes) {
Ok(key) => {
self.signing_key = Some(key);
}
Err(e) => {
log::warn!("Signing key not loaded from bytes: {}", e);
}
}
self
}
pub fn output(&mut self, path: &str) -> &mut Self {
self.output = Some(PathBuf::from(path));
self
}
pub fn build(&self) -> Result<PathBuf, String> {
log::info!("Building plugin '{}' in release mode...", self.name);
let status = std::process::Command::new("cargo")
.args(["build", "--release"])
.status()
.map_err(|e| format!("Failed to run cargo build: {}", e))?;
if !status.success() {
return Err("cargo build --release failed".to_string());
}
let dll_path = self.locate_dll()?;
let dll_dest_name = dll_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("plugin.dll");
let staging = tempfile::tempdir().map_err(|e| format!("Cannot create temp dir: {}", e))?;
let staging_path = staging.path();
std::fs::copy(&dll_path, staging_path.join(dll_dest_name))
.map_err(|e| format!("Cannot copy DLL: {}", e))?;
for dir in &self.extra_dirs {
let src = Path::new(dir);
if src.exists() {
let dst = staging_path.join(dir);
copy_dir_all(src, &dst)?;
} else {
log::warn!("Extra directory '{}' not found, skipping", dir);
}
}
let mut dll_hashes = Vec::new();
let dll_hash = hash_file(&dll_path).map_err(|e| format!("Cannot hash DLL: {}", e))?;
dll_hashes.push(dll_hash);
for dir in &self.extra_dirs {
let dll_dir = staging_path.join(dir);
if dll_dir.exists() {
collect_dll_hashes(&dll_dir, &mut dll_hashes)?;
}
}
let mut manifest = PluginManifest {
name: self.name.clone(),
author: self.author.clone(),
version: self.version.clone(),
description: self.description.clone(),
github_link: self.github_link.clone(),
signature: None,
dll_hashes: Some(dll_hashes.clone()),
};
if let Some(key) = &self.signing_key {
let payload = manifest.signing_payload();
let sig = sign_payload(key, payload.as_bytes());
manifest.signature = Some(sig);
log::info!("Plugin signed");
} else {
log::info!("Plugin not signed (no signing key provided)");
}
manifest
.write_to_yaml(&staging_path.join("plugin.yml"))
.map_err(|e| format!("Cannot write plugin.yml: {}", e))?;
let output_path = self
.output
.clone()
.unwrap_or_else(|| PathBuf::from(format!("target/{}-{}.zip", self.name, self.version)));
packaging::create_zip(staging_path, &output_path)?;
log::info!("Plugin packaged: {}", output_path.display());
Ok(output_path)
}
fn locate_dll(&self) -> Result<PathBuf, String> {
if let Some(path) = &self.dll_path {
if path.exists() {
return Ok(path.clone());
}
return Err(format!(
"Specified DLL path does not exist: {}",
path.display()
));
}
let release_path = PathBuf::from(format!("target/release/{}.dll", self.dll_name));
if release_path.exists() {
return Ok(release_path);
}
let release_so = PathBuf::from(format!("target/release/lib{}.so", self.dll_name));
if release_so.exists() {
return Ok(release_so);
}
Err(format!(
"Cannot find built DLL. Expected at '{}' or '{}'. \
Make sure 'cargo build --release' completed successfully.",
release_path.display(),
release_so.display(),
))
}
}
fn copy_dir_all(src: &Path, dst: &Path) -> Result<(), String> {
std::fs::create_dir_all(dst)
.map_err(|e| format!("Cannot create dir '{}': {}", dst.display(), e))?;
for entry in
std::fs::read_dir(src).map_err(|e| format!("Cannot read dir '{}': {}", src.display(), e))?
{
let entry = entry.map_err(|e| format!("Dir entry error: {}", e))?;
let ty = entry
.file_type()
.map_err(|e| format!("File type error: {}", e))?;
let src_path = entry.path();
let file_name = src_path
.file_name()
.ok_or_else(|| "Invalid filename".to_string())?;
let dst_path = dst.join(file_name);
if ty.is_dir() {
copy_dir_all(&src_path, &dst_path)?;
} else {
std::fs::copy(&src_path, &dst_path)
.map_err(|e| format!("Cannot copy '{}': {}", src_path.display(), e))?;
}
}
Ok(())
}
fn collect_dll_hashes(dir: &Path, hashes: &mut Vec<String>) -> Result<(), String> {
for entry in std::fs::read_dir(dir).map_err(|e| format!("Cannot read dir: {}", e))? {
let entry = entry.map_err(|e| format!("Dir entry error: {}", e))?;
let path = entry.path();
if path.extension().is_some_and(|ext| ext == "dll") {
let hash = hash_file(&path)?;
hashes.push(hash);
}
if entry.file_type().map(|t| t.is_dir()).unwrap_or(false) {
collect_dll_hashes(&path, hashes)?;
}
}
Ok(())
}