use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::{Mutex, OnceLock};
use ed25519_dalek::SigningKey;
use crate::signing::sign_dylib;
pub fn dylib_fixture(plugin_dir: impl Into<PathBuf>) -> DylibFixtureBuilder {
DylibFixtureBuilder {
plugin_dir: plugin_dir.into(),
release: !cfg!(debug_assertions),
signing_key: None,
}
}
pub struct DylibFixtureBuilder {
plugin_dir: PathBuf,
release: bool,
signing_key: Option<SigningKey>,
}
impl DylibFixtureBuilder {
pub fn with_release(mut self, release: bool) -> Self {
self.release = release;
self
}
pub fn signed_with(mut self, key: &SigningKey) -> Self {
self.signing_key = Some(key.clone());
self
}
pub fn build(self) -> DylibFixture {
let cache = cache();
let key = CacheKey {
plugin_dir: self.plugin_dir.clone(),
release: self.release,
};
{
let guard = cache.lock().expect("dylib fixture cache poisoned");
if let Some(existing) = guard.get(&key) {
return existing.clone();
}
}
let fixture = build_uncached(&self.plugin_dir, self.release);
if let Some(signing_key) = &self.signing_key {
sign_dylib(&fixture.dylib_path, signing_key)
.expect("sign_dylib failed for fixture plugin");
}
cache
.lock()
.expect("dylib fixture cache poisoned")
.insert(key, fixture.clone());
fixture
}
}
#[derive(Debug, Clone)]
pub struct DylibFixture {
plugin_output_dir: PathBuf,
dylib_path: PathBuf,
}
impl DylibFixture {
pub fn dir(&self) -> &Path {
&self.plugin_output_dir
}
pub fn dylib_path(&self) -> &Path {
&self.dylib_path
}
}
#[derive(Hash, PartialEq, Eq, Clone)]
struct CacheKey {
plugin_dir: PathBuf,
release: bool,
}
fn cache() -> &'static Mutex<HashMap<CacheKey, DylibFixture>> {
static CACHE: OnceLock<Mutex<HashMap<CacheKey, DylibFixture>>> = OnceLock::new();
CACHE.get_or_init(|| Mutex::new(HashMap::new()))
}
fn dylib_extension() -> &'static str {
if cfg!(target_os = "macos") {
"dylib"
} else if cfg!(target_os = "windows") {
"dll"
} else {
"so"
}
}
fn build_uncached(plugin_dir: &Path, release: bool) -> DylibFixture {
let manifest = plugin_dir.join("Cargo.toml");
assert!(manifest.exists(), "no Cargo.toml at {}", manifest.display());
let mut cmd = Command::new("cargo");
cmd.arg("build").arg("--manifest-path").arg(&manifest);
if release {
cmd.arg("--release");
}
let output = cmd.output().expect("failed to spawn cargo build");
assert!(
output.status.success(),
"cargo build of {} failed:\n{}",
plugin_dir.display(),
String::from_utf8_lossy(&output.stderr),
);
let profile = if release { "release" } else { "debug" };
let plugin_output_dir = plugin_dir.join("target").join(profile);
let ext = dylib_extension();
let dylib_path = std::fs::read_dir(&plugin_output_dir)
.unwrap_or_else(|e| panic!("read_dir {}: {e}", plugin_output_dir.display()))
.filter_map(|e| e.ok())
.map(|e| e.path())
.find(|p| p.extension().and_then(|s| s.to_str()) == Some(ext))
.unwrap_or_else(|| {
panic!(
"build succeeded but no .{} file found in {}",
ext,
plugin_output_dir.display()
)
});
DylibFixture {
plugin_output_dir,
dylib_path,
}
}