fusabi_plugin_runtime/
loader.rs

1//! Plugin loading and compilation.
2
3use std::path::{Path, PathBuf};
4
5use fusabi_host::{
6    compile_source, compile_file, validate_bytecode, CompileOptions,
7    EngineConfig,
8};
9
10use crate::error::{Error, Result};
11use crate::manifest::{ApiVersion, Manifest};
12use crate::plugin::{Plugin, PluginHandle};
13
14/// Configuration for the plugin loader.
15#[derive(Debug, Clone)]
16pub struct LoaderConfig {
17    /// Default engine configuration for plugins.
18    pub engine_config: EngineConfig,
19    /// Compilation options.
20    pub compile_options: CompileOptions,
21    /// Host API version.
22    pub host_api_version: ApiVersion,
23    /// Base path for resolving relative paths.
24    pub base_path: Option<PathBuf>,
25    /// Whether to automatically start plugins after loading.
26    pub auto_start: bool,
27    /// Whether to validate manifests strictly.
28    pub strict_validation: bool,
29}
30
31impl Default for LoaderConfig {
32    fn default() -> Self {
33        Self {
34            engine_config: EngineConfig::default(),
35            compile_options: CompileOptions::default(),
36            host_api_version: ApiVersion::default(),
37            base_path: None,
38            auto_start: true,
39            strict_validation: true,
40        }
41    }
42}
43
44impl LoaderConfig {
45    /// Create a new loader configuration.
46    pub fn new() -> Self {
47        Self::default()
48    }
49
50    /// Set the engine configuration.
51    pub fn with_engine_config(mut self, config: EngineConfig) -> Self {
52        self.engine_config = config;
53        self
54    }
55
56    /// Set the compile options.
57    pub fn with_compile_options(mut self, options: CompileOptions) -> Self {
58        self.compile_options = options;
59        self
60    }
61
62    /// Set the host API version.
63    pub fn with_host_api_version(mut self, version: ApiVersion) -> Self {
64        self.host_api_version = version;
65        self
66    }
67
68    /// Set the base path.
69    pub fn with_base_path(mut self, path: impl Into<PathBuf>) -> Self {
70        self.base_path = Some(path.into());
71        self
72    }
73
74    /// Set auto-start behavior.
75    pub fn with_auto_start(mut self, auto_start: bool) -> Self {
76        self.auto_start = auto_start;
77        self
78    }
79
80    /// Set strict validation.
81    pub fn with_strict_validation(mut self, strict: bool) -> Self {
82        self.strict_validation = strict;
83        self
84    }
85
86    /// Create a strict loader config.
87    pub fn strict() -> Self {
88        Self {
89            engine_config: EngineConfig::strict(),
90            compile_options: CompileOptions::production(),
91            host_api_version: ApiVersion::default(),
92            base_path: None,
93            auto_start: false,
94            strict_validation: true,
95        }
96    }
97}
98
99/// Plugin loader for loading plugins from manifests and source files.
100pub struct PluginLoader {
101    config: LoaderConfig,
102}
103
104impl PluginLoader {
105    /// Create a new plugin loader.
106    pub fn new(config: LoaderConfig) -> Result<Self> {
107        Ok(Self { config })
108    }
109
110    /// Get the loader configuration.
111    pub fn config(&self) -> &LoaderConfig {
112        &self.config
113    }
114
115    /// Load a plugin from a manifest file.
116    #[cfg(feature = "serde")]
117    pub fn load_from_manifest(&self, manifest_path: impl AsRef<Path>) -> Result<PluginHandle> {
118        let manifest_path = self.resolve_path(manifest_path.as_ref());
119        let manifest = Manifest::from_file(&manifest_path)?;
120
121        self.load_manifest(manifest, Some(manifest_path))
122    }
123
124    /// Load a plugin from a manifest object.
125    pub fn load_manifest(
126        &self,
127        manifest: Manifest,
128        manifest_path: Option<PathBuf>,
129    ) -> Result<PluginHandle> {
130        // Validate manifest
131        if self.config.strict_validation {
132            manifest.validate()?;
133        }
134
135        // Check API version compatibility
136        if !manifest.is_compatible_with_host(&self.config.host_api_version) {
137            return Err(Error::api_version_mismatch(
138                manifest.api_version.to_string(),
139                self.config.host_api_version.to_string(),
140            ));
141        }
142
143        // Create plugin
144        let plugin = Plugin::new(manifest.clone());
145
146        // Resolve entry point path
147        let entry_path = manifest.entry_point().map(|p| {
148            if let Some(ref manifest_path) = manifest_path {
149                manifest_path.parent().unwrap_or(Path::new(".")).join(p)
150            } else {
151                self.resolve_path(Path::new(p))
152            }
153        });
154
155        // Load source or bytecode
156        if let Some(ref entry_path) = entry_path {
157            if manifest.uses_source() {
158                self.compile_and_load(&plugin, entry_path)?;
159            } else {
160                self.load_bytecode(&plugin, entry_path)?;
161            }
162        }
163
164        // Build engine config with required capabilities
165        let engine_config = self.build_engine_config(&manifest)?;
166
167        // Initialize plugin
168        plugin.initialize(engine_config)?;
169
170        // Auto-start if configured
171        if self.config.auto_start {
172            plugin.start()?;
173        }
174
175        Ok(PluginHandle::new(plugin))
176    }
177
178    /// Load a plugin from a source file directly.
179    pub fn load_source(&self, source_path: impl AsRef<Path>) -> Result<PluginHandle> {
180        let source_path = self.resolve_path(source_path.as_ref());
181
182        // Read and parse source for embedded manifest
183        let source = std::fs::read_to_string(&source_path)?;
184
185        // Create a minimal manifest
186        let name = source_path
187            .file_stem()
188            .and_then(|s| s.to_str())
189            .unwrap_or("unnamed")
190            .to_string();
191
192        let manifest = Manifest::new(name, "0.0.0");
193
194        // Create plugin
195        let plugin = Plugin::new(manifest);
196
197        // Compile source
198        let compile_result = compile_source(&source, &self.config.compile_options)?;
199        plugin.set_bytecode(compile_result.bytecode);
200
201        // Initialize with default config
202        plugin.initialize(self.config.engine_config.clone())?;
203
204        // Auto-start if configured
205        if self.config.auto_start {
206            plugin.start()?;
207        }
208
209        Ok(PluginHandle::new(plugin))
210    }
211
212    /// Load a plugin from bytecode directly.
213    pub fn load_bytecode_file(&self, bytecode_path: impl AsRef<Path>) -> Result<PluginHandle> {
214        let bytecode_path = self.resolve_path(bytecode_path.as_ref());
215
216        // Read bytecode
217        let bytecode = std::fs::read(&bytecode_path)?;
218
219        // Validate bytecode
220        let metadata = validate_bytecode(&bytecode)?;
221
222        // Create manifest from bytecode metadata
223        let name = bytecode_path
224            .file_stem()
225            .and_then(|s| s.to_str())
226            .unwrap_or("unnamed")
227            .to_string();
228
229        let manifest = Manifest::new(name, metadata.compiler_version.clone());
230
231        // Create plugin
232        let plugin = Plugin::new(manifest);
233        plugin.set_bytecode(bytecode);
234
235        // Initialize with default config
236        plugin.initialize(self.config.engine_config.clone())?;
237
238        // Auto-start if configured
239        if self.config.auto_start {
240            plugin.start()?;
241        }
242
243        Ok(PluginHandle::new(plugin))
244    }
245
246    /// Reload a plugin.
247    pub fn reload(&self, plugin: &PluginHandle) -> Result<()> {
248        plugin.inner().reload()
249    }
250
251    // Helper methods
252
253    fn resolve_path(&self, path: &Path) -> PathBuf {
254        if path.is_absolute() {
255            path.to_path_buf()
256        } else if let Some(ref base) = self.config.base_path {
257            base.join(path)
258        } else {
259            path.to_path_buf()
260        }
261    }
262
263    fn compile_and_load(&self, plugin: &Plugin, source_path: &Path) -> Result<()> {
264        let compile_result = compile_file(source_path, &self.config.compile_options)
265            .map_err(|e: fusabi_host::Error| Error::Compilation(e.to_string()))?;
266
267        plugin.set_bytecode(compile_result.bytecode);
268
269        // Log warnings
270        for warning in &compile_result.warnings {
271            tracing::warn!("Plugin {}: {}", plugin.name(), warning.message);
272        }
273
274        Ok(())
275    }
276
277    fn load_bytecode(&self, plugin: &Plugin, bytecode_path: &Path) -> Result<()> {
278        let bytecode = std::fs::read(bytecode_path)?;
279
280        // Validate
281        validate_bytecode(&bytecode)?;
282
283        plugin.set_bytecode(bytecode);
284        Ok(())
285    }
286
287    fn build_engine_config(&self, manifest: &Manifest) -> Result<EngineConfig> {
288        // Start with base config
289        let mut config = self.config.engine_config.clone();
290
291        // Add required capabilities
292        let mut caps = config.capabilities.clone();
293        for cap_name in &manifest.capabilities {
294            let cap = fusabi_host::Capability::from_name(cap_name)
295                .ok_or_else(|| Error::invalid_manifest(format!("unknown capability: {}", cap_name)))?;
296            caps.grant(cap);
297        }
298        config.capabilities = caps;
299
300        Ok(config)
301    }
302}
303
304impl std::fmt::Debug for PluginLoader {
305    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
306        f.debug_struct("PluginLoader")
307            .field("config", &self.config)
308            .finish()
309    }
310}
311
312#[cfg(test)]
313mod tests {
314    use super::*;
315    use crate::manifest::ManifestBuilder;
316
317    #[test]
318    fn test_loader_config_builder() {
319        let config = LoaderConfig::new()
320            .with_auto_start(false)
321            .with_strict_validation(true);
322
323        assert!(!config.auto_start);
324        assert!(config.strict_validation);
325    }
326
327    #[test]
328    fn test_loader_creation() {
329        let loader = PluginLoader::new(LoaderConfig::default()).unwrap();
330        assert!(loader.config().auto_start);
331    }
332
333    #[test]
334    fn test_load_manifest() {
335        let loader = PluginLoader::new(
336            LoaderConfig::new().with_auto_start(false),
337        )
338        .unwrap();
339
340        let manifest = ManifestBuilder::new("test-plugin", "1.0.0")
341            .source("test.fsx")
342            .build_unchecked();
343
344        // This will fail because the source file doesn't exist,
345        // but it tests the loading logic
346        let result = loader.load_manifest(manifest, None);
347
348        // Should fail on missing source file
349        assert!(result.is_err());
350    }
351
352    #[test]
353    fn test_api_version_check() {
354        let loader = PluginLoader::new(
355            LoaderConfig::new()
356                .with_host_api_version(ApiVersion::new(0, 18, 0))
357                .with_auto_start(false),
358        )
359        .unwrap();
360
361        // Plugin requiring newer version should fail
362        let manifest = ManifestBuilder::new("test", "1.0.0")
363            .api_version(ApiVersion::new(1, 0, 0))
364            .source("test.fsx")
365            .build_unchecked();
366
367        let result = loader.load_manifest(manifest, None);
368        assert!(matches!(result, Err(Error::ApiVersionMismatch { .. })));
369    }
370}