Skip to main content

fresh/services/plugins/
embedded.rs

1//! Embedded plugins support
2//!
3//! When the `embed-plugins` feature is enabled, this module provides access to plugins
4//! that are compiled directly into the binary. This is useful for cargo-binstall
5//! distributions where the plugins directory would otherwise be missing.
6//!
7//! The plugins are extracted to a temporary directory at runtime and loaded from there.
8
9use include_dir::{include_dir, Dir};
10use std::path::PathBuf;
11use std::sync::OnceLock;
12
13/// The plugins directory embedded at compile time
14static EMBEDDED_PLUGINS: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/plugins");
15
16/// Cached path to the extracted plugins directory
17static EXTRACTED_PLUGINS_DIR: OnceLock<PathBuf> = OnceLock::new();
18
19/// Get the path to the embedded plugins directory.
20///
21/// On first call, this extracts the embedded plugins to a cache directory.
22/// The cache is content-addressed, so unchanged plugins are reused across runs.
23///
24/// Returns `None` if extraction fails.
25pub fn get_embedded_plugins_dir() -> Option<&'static PathBuf> {
26    EXTRACTED_PLUGINS_DIR.get_or_init(|| match extract_plugins() {
27        Ok(path) => path,
28        Err(e) => {
29            tracing::error!("Failed to extract embedded plugins: {}", e);
30            PathBuf::new()
31        }
32    });
33
34    let path = EXTRACTED_PLUGINS_DIR.get()?;
35    if path.exists()
36        && path
37            .read_dir()
38            .map(|mut d| d.next().is_some())
39            .unwrap_or(false)
40    {
41        Some(path)
42    } else {
43        None
44    }
45}
46
47/// Content hash of embedded plugins, computed at build time
48const PLUGINS_CONTENT_HASH: &str = include_str!(concat!(env!("OUT_DIR"), "/plugins_hash.txt"));
49
50/// Get the cache directory for extracted plugins
51fn get_cache_dir() -> Option<PathBuf> {
52    dirs::cache_dir().map(|p| p.join("fresh").join("embedded-plugins"))
53}
54
55/// Extract embedded plugins to the cache directory
56fn extract_plugins() -> Result<PathBuf, std::io::Error> {
57    let cache_base = get_cache_dir().ok_or_else(|| {
58        std::io::Error::new(
59            std::io::ErrorKind::NotFound,
60            "Could not determine cache directory",
61        )
62    })?;
63
64    let content_hash = PLUGINS_CONTENT_HASH.trim();
65    let cache_dir = cache_base.join(content_hash);
66
67    // Check if already extracted
68    if cache_dir.exists() && cache_dir.read_dir()?.next().is_some() {
69        tracing::info!("Using cached embedded plugins from: {:?}", cache_dir);
70        return Ok(cache_dir);
71    }
72
73    tracing::info!("Extracting embedded plugins to: {:?}", cache_dir);
74
75    // Clean up old cache versions (move to trash for safety)
76    if cache_base.exists() {
77        for entry in std::fs::read_dir(&cache_base)? {
78            let entry = entry?;
79            if entry.file_name() != content_hash {
80                // Best-effort cleanup of old cache versions; failure is non-fatal.
81                #[allow(clippy::let_underscore_must_use)]
82                let _ = trash::delete(entry.path());
83            }
84        }
85    }
86
87    extract_dir_recursive(&EMBEDDED_PLUGINS, &cache_dir)?;
88
89    tracing::info!(
90        "Successfully extracted {} embedded plugin files",
91        count_files(&EMBEDDED_PLUGINS)
92    );
93
94    Ok(cache_dir)
95}
96
97/// Recursively extract a directory and its contents
98fn extract_dir_recursive(dir: &Dir<'_>, target_path: &std::path::Path) -> std::io::Result<()> {
99    std::fs::create_dir_all(target_path)?;
100
101    // Extract files
102    for file in dir.files() {
103        let file_path = target_path.join(file.path().file_name().unwrap_or_default());
104        std::fs::write(&file_path, file.contents())?;
105        tracing::debug!("Extracted: {:?}", file_path);
106    }
107
108    // Recursively extract subdirectories
109    for subdir in dir.dirs() {
110        let subdir_name = subdir.path().file_name().unwrap_or_default();
111        let subdir_path = target_path.join(subdir_name);
112        extract_dir_recursive(subdir, &subdir_path)?;
113    }
114
115    Ok(())
116}
117
118/// Count total files in embedded directory (for logging)
119fn count_files(dir: &Dir<'_>) -> usize {
120    let mut count = dir.files().count();
121    for subdir in dir.dirs() {
122        count += count_files(subdir);
123    }
124    count
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    #[test]
132    fn test_embedded_plugins_exist() {
133        // Verify that plugins are embedded
134        assert!(EMBEDDED_PLUGINS.files().count() > 0 || EMBEDDED_PLUGINS.dirs().count() > 0);
135    }
136
137    #[test]
138    fn test_extract_plugins() {
139        let path = get_embedded_plugins_dir();
140        assert!(path.is_some());
141        let path = path.unwrap();
142        assert!(path.exists());
143        assert!(path.is_dir());
144
145        // Check that some plugin files exist
146        let entries: Vec<_> = std::fs::read_dir(path).unwrap().collect();
147        assert!(!entries.is_empty());
148    }
149}