2658d0ebfa
Add new `build_status` types module for tracking pipeline stage execution status across prebake, bake, and finalize phases. Introduce `NotificationCore` dispatcher that processes notification methods by priority groups with fallback on failure. Status files written to `/tmp/.hbwstatus` in JSON lines format.
268 lines
9.0 KiB
Rust
268 lines
9.0 KiB
Rust
use crate::constant::{FINALIZE_PLUGIN_MANIFEST, FINALIZE_PLUGIN_MANIFEST_EXTENSION};
|
|
use crate::finalize::RawPluginMap;
|
|
use crate::finalize::plugin::metadata::{PluginMetadata, PluginType};
|
|
use crate::{EventSender, ExecutionContext, finalize::error::PluginError};
|
|
use async_trait::async_trait;
|
|
use std::collections::HashMap;
|
|
use std::path::{PathBuf, Path};
|
|
use std::sync::Arc;
|
|
|
|
mod dylib;
|
|
mod internal;
|
|
mod metadata;
|
|
mod rhai;
|
|
mod shell;
|
|
pub mod fetch;
|
|
|
|
#[derive(Clone)]
|
|
pub struct FinalizePlugin {
|
|
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.to_string()),
|
|
}
|
|
})
|
|
}
|
|
|
|
/// 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
|
|
);
|
|
return 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) -> Result<PluginMap, PluginError> {
|
|
let mut plugins: PluginMap = HashMap::new();
|
|
|
|
// Register internal plugin
|
|
for i in internal::get_internal_plugin() {
|
|
log::debug!("Registering internal plugin {}", i.name);
|
|
plugins.insert(i.name.to_string(), FinalizePlugin::new(i.entry));
|
|
}
|
|
|
|
// Register external plugin
|
|
for (name, data) in external_plugins.into_iter() {
|
|
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;
|
|
}
|
|
let metadata = read_manifest(config.runtime.fspath, &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),
|
|
);
|
|
}
|
|
|
|
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
|
|
}
|