use include_dir::{include_dir, Dir};
use std::path::PathBuf;
use std::sync::OnceLock;
static EMBEDDED_PLUGINS: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/plugins");
static EXTRACTED_PLUGINS_DIR: OnceLock<PathBuf> = OnceLock::new();
pub fn get_embedded_plugins_dir() -> Option<&'static PathBuf> {
EXTRACTED_PLUGINS_DIR.get_or_init(|| match extract_plugins() {
Ok(path) => path,
Err(e) => {
tracing::error!("Failed to extract embedded plugins: {}", e);
PathBuf::new()
}
});
let path = EXTRACTED_PLUGINS_DIR.get()?;
if path.exists()
&& path
.read_dir()
.map(|mut d| d.next().is_some())
.unwrap_or(false)
{
Some(path)
} else {
None
}
}
const PLUGINS_CONTENT_HASH: &str = include_str!(concat!(env!("OUT_DIR"), "/plugins_hash.txt"));
fn get_cache_dir() -> Option<PathBuf> {
dirs::cache_dir().map(|p| p.join("fresh").join("embedded-plugins"))
}
fn extract_plugins() -> Result<PathBuf, std::io::Error> {
let cache_base = get_cache_dir().ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::NotFound,
"Could not determine cache directory",
)
})?;
let content_hash = PLUGINS_CONTENT_HASH.trim();
let cache_dir = cache_base.join(content_hash);
let marker = cache_dir.join(".extracted");
if marker.exists() {
tracing::info!("Using cached embedded plugins from: {:?}", cache_dir);
return Ok(cache_dir);
}
tracing::info!("Extracting embedded plugins to: {:?}", cache_dir);
std::fs::create_dir_all(&cache_base)?;
let pid = std::process::id();
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let tmp_dir = cache_base.join(format!(".pending.{}.{}", pid, nanos));
extract_dir_recursive(&EMBEDDED_PLUGINS, &tmp_dir)?;
std::fs::write(tmp_dir.join(".extracted"), b"")?;
let publish = |tmp_dir: &std::path::Path| std::fs::rename(tmp_dir, &cache_dir);
let result = match publish(&tmp_dir) {
Ok(()) => Ok(cache_dir.clone()),
Err(_) if marker.exists() => {
#[allow(clippy::let_underscore_must_use)]
let _ = std::fs::remove_dir_all(&tmp_dir);
Ok(cache_dir.clone())
}
Err(_) => {
let stale = cache_base.join(format!(".stale.{}.{}", pid, nanos));
#[allow(clippy::let_underscore_must_use)]
let _ = std::fs::rename(&cache_dir, &stale);
#[allow(clippy::let_underscore_must_use)]
let _ = std::fs::remove_dir_all(&stale);
match publish(&tmp_dir) {
Ok(()) => Ok(cache_dir.clone()),
Err(e) => {
#[allow(clippy::let_underscore_must_use)]
let _ = std::fs::remove_dir_all(&tmp_dir);
if marker.exists() {
Ok(cache_dir.clone())
} else {
Err(e)
}
}
}
}
};
if result.is_ok() {
tracing::info!(
"Successfully extracted {} embedded plugin files",
count_files(&EMBEDDED_PLUGINS)
);
if let Ok(entries) = std::fs::read_dir(&cache_base) {
for entry in entries.flatten() {
let name_os = entry.file_name();
let name = name_os.to_string_lossy();
if name == content_hash {
continue;
}
if name.starts_with(".pending.") || name.starts_with(".stale.") {
continue;
}
#[allow(clippy::let_underscore_must_use)]
let _ = trash::delete(entry.path());
}
}
}
result
}
fn extract_dir_recursive(dir: &Dir<'_>, target_path: &std::path::Path) -> std::io::Result<()> {
std::fs::create_dir_all(target_path)?;
for file in dir.files() {
let file_path = target_path.join(file.path().file_name().unwrap_or_default());
std::fs::write(&file_path, file.contents())?;
tracing::debug!("Extracted: {:?}", file_path);
}
for subdir in dir.dirs() {
let subdir_name = subdir.path().file_name().unwrap_or_default();
let subdir_path = target_path.join(subdir_name);
extract_dir_recursive(subdir, &subdir_path)?;
}
Ok(())
}
fn count_files(dir: &Dir<'_>) -> usize {
let mut count = dir.files().count();
for subdir in dir.dirs() {
count += count_files(subdir);
}
count
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_embedded_plugins_exist() {
assert!(EMBEDDED_PLUGINS.files().count() > 0 || EMBEDDED_PLUGINS.dirs().count() > 0);
}
#[test]
fn test_extract_plugins() {
let path = get_embedded_plugins_dir();
assert!(path.is_some());
let path = path.unwrap();
assert!(path.exists());
assert!(path.is_dir());
let entries: Vec<_> = std::fs::read_dir(path).unwrap().collect();
assert!(!entries.is_empty());
}
}