Files
honey-biscuit-workshop/workshop-baker/src/finalize/plugin.rs
T
Catty Steve 28abc5d207 refactor: replace internal fetch utilities with ResourceRegistry
Remove the monolithic utils::fetch module (HTTP download, git clone,
checksum, extraction) and utils::fetch_plugin module from the baker.
Introduce a ResourceRegistry type to centrally track pipeline
resources by name and path.

The actual fetching logic is now delegated to the workshop-getterurl
crate, which gains optional md5 and sha1 hash support behind default
features. Also flatten the builtin module in workshop-getterurl.
2026-05-19 23:22:59 +08:00

321 lines
11 KiB
Rust

use crate::finalize::RawPluginMap;
use crate::finalize::constant::{FINALIZE_PLUGIN_MANIFEST, FINALIZE_PLUGIN_MANIFEST_EXTENSION};
use crate::finalize::plugin::metadata::{PluginMetadata, PluginType};
use workshop_engine::{EventSender, ExecutionContext};
use crate::{ finalize::error::PluginError};
use crate::types::resource::ResourceRegistry;
use async_trait::async_trait;
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::sync::Arc;
mod dylib;
mod internal;
mod metadata;
mod rhai;
mod shell;
#[derive(Clone)]
pub struct FinalizePlugin {
pub metadata: PluginMetadata,
argument: serde_yaml::Value,
pub entry: Arc<dyn AsyncPluginFn>,
}
impl FinalizePlugin {
pub fn new(entry: Box<dyn AsyncPluginFn>) -> Self {
Self {
metadata: PluginMetadata::default(),
argument: serde_yaml::Value::Mapping(Default::default()),
entry: Arc::from(entry),
}
}
pub fn with_metadata(mut self, metadata: PluginMetadata) -> Self {
self.metadata = metadata;
self
}
pub fn with_argument(mut self, argument: serde_yaml::Value) -> Self {
self.argument = argument;
self
}
pub async fn call(
&self,
runtime_argument: serde_yaml::Value,
ctx: &ExecutionContext,
event_tx: &EventSender,
) -> PluginResult {
// shallow merge argument
let _argument = match (&self.argument, runtime_argument) {
(serde_yaml::Value::Mapping(base), serde_yaml::Value::Mapping(runtime)) => {
let mut merged = base.clone();
for (k, v) in runtime {
merged.insert(k.clone(), v.clone());
}
serde_yaml::Value::Mapping(merged)
}
_ => self.argument.clone(), // If not both are mappings, just use the original argument
};
self.entry
.call(&self.metadata, _argument, ctx, event_tx)
.await
}
}
pub type PluginMap = HashMap<String, FinalizePlugin>;
type PluginResult = Result<(), PluginError>;
#[async_trait]
pub trait AsyncPluginFn: Send + Sync {
async fn call(
&self,
metadata: &PluginMetadata,
argument: serde_yaml::Value,
ctx: &ExecutionContext,
event_tx: &EventSender,
) -> PluginResult;
}
/// Reads and parses a plugin's manifest file.
///
/// # Arguments
///
/// * `path` - Optional filesystem path to the plugin directory
/// * `name` - Name of the plugin (used for error reporting)
///
/// # Returns
///
/// Returns parsed [`PluginMetadata`] on success, or a [`PluginError`] if the manifest
/// cannot be read or parsed.
fn read_manifest(path: Option<PathBuf>, name: &str) -> Result<PluginMetadata, PluginError> {
let path = path.ok_or_else(|| PluginError::PluginUnavailable {
name: name.to_string(),
reason: "Plugin not present in filesystem.".to_string(),
})?;
// Extract content from manifest.yml or manifest.yaml
let manifest_content = 'load: {
for ext in FINALIZE_PLUGIN_MANIFEST_EXTENSION {
match load_manifest(&path, name, ext) {
Ok(content) => break 'load content,
Err(PluginError::ManifestNotFound(_)) => {}
Err(e) => return Err(e),
}
}
return Err(PluginError::ManifestNotFound(path));
};
log::debug!("[plugin:{}] Parsing manifest content", name);
let mut metadata: PluginMetadata =
serde_yaml::from_str(manifest_content.as_str()).map_err(|e| {
PluginError::PluginUnavailable {
name: name.to_string(),
reason: format!("Failed to parse plugin manifest: {}", e),
}
})?;
metadata.fspath = path;
Ok(metadata)
}
fn load_manifest(path: &Path, name: &str, suffix: &str) -> Result<String, PluginError> {
let manifest_path = path.join(format!("{}{}", FINALIZE_PLUGIN_MANIFEST, suffix));
log::debug!(
"[plugin:{}] Reading manifest from {}",
name,
manifest_path.display()
);
if !manifest_path.exists() {
return Err(PluginError::ManifestNotFound(manifest_path));
}
std::fs::read_to_string(&manifest_path).map_err(|e| PluginError::PluginUnavailable {
name: name.to_string(),
reason: format!(
"Failed to read plugin manifest from {}: {}",
manifest_path.display(),
e
),
})
}
/// Determines the plugin type based on the manifest type or file extension.
///
/// The function first returns `original` if it's not [`PluginType::Unknown`].
/// Otherwise, it infers the type from the entrypoint file extension:
/// - `.sh` / `.bash` → [`PluginType::Shell`]
/// - `.rhai` → [`PluginType::Rhai`]
/// - `.so` / `.dll` / `.dylib` → [`PluginType::Dylib`]
///
/// # Arguments
///
/// * `original` - Plugin type as specified in the manifest
/// * `entrypoint` - Path or filename of the plugin entrypoint
///
/// # Returns
///
/// The inferred [`PluginType`].
fn get_plugin_type(original: PluginType, entrypoint: &str) -> PluginType {
if original != PluginType::Unknown {
return original;
}
let _suffix = entrypoint.rsplit('.').next().unwrap_or("");
match _suffix {
"sh" | "bash" => PluginType::Shell,
"rhai" => PluginType::Rhai,
"so" | "dll" | "dylib" => PluginType::Dylib,
_ => PluginType::Unknown,
}
}
/// Creates a plugin entry function instance based on the plugin type.
///
/// # Arguments
///
/// * `plugin_type` - The type of plugin to instantiate
/// * `name` - Name of the plugin (used for error reporting)
///
/// # Returns
///
/// Returns a boxed [`AsyncPluginFn`] on success, or a [`PluginError`] if the plugin type
/// is [`PluginType::Unknown`] and cannot be inferred.
fn get_entry(plugin_type: PluginType, name: &str) -> Result<Box<dyn AsyncPluginFn>, PluginError> {
match plugin_type {
PluginType::Shell => Ok(Box::new(shell::Shell)),
PluginType::Rhai => Ok(Box::new(rhai::Rhai)),
PluginType::Dylib => Ok(Box::new(dylib::Dylib)),
PluginType::Unknown => {
log::error!(
"Plugin {} has unknown plugin type and cannot be inferred from entrypoint.",
name
);
Err(PluginError::PluginUnavailable {
name: name.to_string(),
reason: "Unknown plugin type and cannot be inferred from entrypoint".to_string(),
})
}
}
}
/// Registers all available plugins (internal and external) into a plugin map.
///
/// Internal plugins are registered first, followed by external plugins loaded from
/// the provided [`RawPluginMap`]. Plugins that are unavailable (e.g., missing runtime)
/// are logged and skipped.
///
/// # Arguments
///
/// * `external_plugins` - Map of external plugin configurations
///
/// # Returns
///
/// Returns a [`PluginMap`] containing all registered plugins, or a [`PluginError`] if
/// a plugin manifest cannot be read or parsed.
pub fn register(
external_plugins: RawPluginMap,
list: Option<&HashSet<String>>,
notification_config: &mut crate::finalize::NotificationConfig,
registry: &ResourceRegistry,
) -> Result<PluginMap, PluginError> {
let mut plugins: PluginMap = HashMap::new();
let mut count = 0;
// Register internal plugin
for i in internal::get_internal_plugin() {
let name = i.name.to_string();
match list {
Some(list) if !list.contains(&name) => {
log::debug!("Plugin {} not in list, skipping", name);
continue;
}
_ => {}
}
log::debug!("Registering internal plugin {}", i.name);
plugins.insert(i.name.to_string(), FinalizePlugin::new(i.entry));
count += 1;
}
// Register external plugin
for (name, data) in external_plugins.into_iter() {
match list {
Some(list) if !list.contains(&name) => {
log::debug!("Plugin {} not in list, skipping", name);
continue;
}
_ => {}
}
log::debug!("Registering external plugin {}", name);
let (_source, config) = data.into_iter().next().unwrap_or_else(|| {
panic!(
"Internal Error: Plugin {} has no source configured, this should have been caught by validation!",
name
)
});
// Assume every plugin prepared (or definitely unavailable) now
if let Some(reason) = config.runtime.error {
log::warn!("Plugin {} is not available: {}", name, reason);
continue;
}
// Resolve plugin path from registry instead of config.runtime.fspath
let plugin_path = registry.resolve(&name).ok_or_else(|| {
PluginError::PluginUnavailable {
name: name.clone(),
reason: "plugin not found in resource registry".to_string(),
}
})?;
let metadata = read_manifest(Some(plugin_path), &name)?;
let plugin_type = get_plugin_type(metadata.plugin_type.clone(), &metadata.entrypoint);
let entry = get_entry(plugin_type, &name)?;
plugins.insert(
name,
FinalizePlugin::new(entry)
.with_metadata(metadata)
.with_argument(config.config),
);
count += 1;
}
// Inject plugin-declared templates as .fallback
for (name, plugin) in &plugins {
if let Some(ref templates) = plugin.metadata.templates {
let key = format!("{}.fallback", name);
for tmpl in templates {
notification_config.templates.insert(key.clone(), tmpl.clone());
}
log::debug!("Registered templates from plugin '{}' as '{}'", name, key);
}
}
log::info!("Registered {} plugins", count);
Ok(plugins)
}
/// Calls a registered plugin by name, passing the configuration, execution context and event sender.
///
/// # Arguments
///
/// * `plugins` - Map of registered plugin names to their [`FinalizePlugin`] instances
/// * `name` - Name of the plugin to invoke
/// * `config` - Configuration value passed to the plugin
/// * `ctx` - Execution context for the plugin
/// * `event_tx` - Event sender for plugin lifecycle events
///
/// # Returns
///
/// Returns `Ok(())` on successful plugin execution, or a [`PluginError`] if the plugin
/// is not found or execution fails.
pub async fn call_named_plugin(
plugins: &PluginMap,
name: &str,
config: serde_yaml::Value,
ctx: &ExecutionContext,
event_tx: &EventSender,
) -> PluginResult {
let plugin = plugins
.get(name)
.ok_or_else(|| PluginError::UnknownPlugin(name.to_string()))?;
plugin.call(config, ctx, event_tx).await
}