use crate::builder::PluginRegistryBuilder;
use crate::dialect::PlatformRegistry;
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
pub const PLUGIN_DIR_ENV: &str = "RAZERI_PLUGIN_DIR";
#[derive(Debug)]
pub struct PluginDirReport {
pub dir: PathBuf,
pub loaded: Vec<crate::dynamic::PluginLoadResult>,
}
#[derive(Debug, thiserror::Error)]
pub enum InstallError {
#[error(
"the plugin directory {dir} was offered after this process had already built its \
dialect registry{earlier}; a plugin directory is installed before the first \
platform name is resolved, never after"
)]
AlreadyBuilt {
dir: PathBuf,
earlier: String,
},
#[error("the plugin directory {dir} cannot be read: {source}")]
Unreadable {
dir: PathBuf,
#[source]
source: std::io::Error,
},
}
struct Installed {
dialects: PlatformRegistry,
report: Option<PluginDirReport>,
}
static INSTALLED: OnceLock<Installed> = OnceLock::new();
pub fn process_registry() -> &'static PlatformRegistry {
&INSTALLED.get_or_init(|| build(plugin_dir_from_environment())).dialects
}
pub fn install_plugin_dir(dir: &Path) -> Result<&'static PluginDirReport, InstallError> {
std::fs::read_dir(dir)
.map_err(|source| InstallError::Unreadable { dir: dir.to_path_buf(), source })?;
let mut built_now = false;
let installed = INSTALLED.get_or_init(|| {
built_now = true;
build(Some(dir.to_path_buf()))
});
match &installed.report {
Some(report) if built_now || same_dir(&report.dir, dir) => Ok(report),
Some(report) => Err(InstallError::AlreadyBuilt {
dir: dir.to_path_buf(),
earlier: format!(" from {}", report.dir.display()),
}),
None => Err(InstallError::AlreadyBuilt { dir: dir.to_path_buf(), earlier: String::new() }),
}
}
pub fn plugin_dir_report() -> Option<&'static PluginDirReport> {
INSTALLED.get().and_then(|installed| installed.report.as_ref())
}
#[must_use]
pub fn plugin_dir_from_environment() -> Option<PathBuf> {
std::env::var_os(PLUGIN_DIR_ENV).filter(|value| !value.is_empty()).map(PathBuf::from)
}
fn build(dir: Option<PathBuf>) -> Installed {
let mut builder = PluginRegistryBuilder::new().with_auto_discovered();
let report = dir.map(|dir| {
let loaded = load_into(&dir, &mut builder);
PluginDirReport { dir, loaded }
});
Installed { dialects: builder.build().dialects, report }
}
#[cfg(all(feature = "dynamic", not(target_arch = "wasm32")))]
fn load_into(
dir: &Path,
builder: &mut PluginRegistryBuilder,
) -> Vec<crate::dynamic::PluginLoadResult> {
crate::dynamic::load_plugins_from_dir(dir, builder)
}
#[cfg(not(all(feature = "dynamic", not(target_arch = "wasm32"))))]
fn load_into(
dir: &Path,
_builder: &mut PluginRegistryBuilder,
) -> Vec<crate::dynamic::PluginLoadResult> {
tracing::warn!(
"a plugin directory was named ({}) but this build cannot load shared libraries",
dir.display()
);
Vec::new()
}
fn same_dir(a: &Path, b: &Path) -> bool {
match (std::fs::canonicalize(a), std::fs::canonicalize(b)) {
(Ok(a), Ok(b)) => a == b,
_ => a == b,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_registry_is_built_once_and_handed_back_every_time() {
let first: *const PlatformRegistry = process_registry();
let second: *const PlatformRegistry = process_registry();
assert!(std::ptr::eq(first, second), "two calls returned two registries");
}
#[test]
fn a_directory_offered_after_the_build_is_refused_unless_it_is_the_same_one() {
let _ = process_registry();
let elsewhere = tempfile::tempdir().unwrap();
let err = install_plugin_dir(elsewhere.path()).unwrap_err();
assert!(matches!(err, InstallError::AlreadyBuilt { .. }), "{err}");
assert!(err.to_string().contains("before the first platform name is resolved"), "{err}");
}
#[test]
fn an_unreadable_directory_is_named_in_the_error() {
let missing = std::env::temp_dir().join("razeri-no-such-plugin-dir-ever");
let err = install_plugin_dir(&missing).unwrap_err();
assert!(matches!(err, InstallError::Unreadable { .. }), "{err}");
assert!(err.to_string().contains("razeri-no-such-plugin-dir-ever"), "{err}");
}
#[test]
fn the_environment_variable_is_read_as_a_path_and_empty_means_unset() {
let expected =
std::env::var_os(PLUGIN_DIR_ENV).filter(|v| !v.is_empty()).map(PathBuf::from);
assert_eq!(plugin_dir_from_environment(), expected);
}
}