use anyhow::{Context, Result};
use once_cell::sync::Lazy;
use regex::Regex;
use std::path::Path;
use thiserror::Error;
use super::manifest_schema::{Manifest, PackageInfo, PackageLanguage, RustRuntime, TaskDefinition};
use super::types::CargoToml;
#[allow(dead_code)]
static PACKAGED_WORKFLOW_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(
r#"#\[(?:packaged_)?workflow\s*\(\s*[^)]*(?:package|name)\s*=\s*"([^"]+)"[^)]*\)\s*\]"#,
)
.expect("Invalid workflow regex pattern - this is a compile-time bug")
});
#[derive(Debug, Error)]
pub enum ManifestError {
#[error("Invalid dependencies JSON for task '{task_id}': {source}")]
InvalidDependencies {
task_id: String,
#[source]
source: serde_json::Error,
},
#[error("Invalid graph data JSON: {source}")]
InvalidGraphData {
#[source]
source: serde_json::Error,
},
#[error("Library error: {message}")]
LibraryError { message: String },
}
pub fn generate_manifest(
cargo_toml: &CargoToml,
so_path: &Path,
target: &Option<String>,
project_path: &Path,
) -> Result<Manifest> {
let package = cargo_toml
.package
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Missing package section in Cargo.toml"))?;
let library_filename = so_path
.file_name()
.ok_or_else(|| anyhow::anyhow!("Invalid so_path"))?
.to_string_lossy()
.to_string();
let (ffi_tasks, _graph_data, package_metadata) =
extract_task_info_and_graph_from_library(so_path, project_path)?;
let target_platform = if let Some(target_triple) = target {
target_triple.clone()
} else {
get_current_platform()
};
let fingerprint = format!(
"sha256:{}:{}:{}",
package.name,
package.version,
package_metadata
.workflow_fingerprint
.as_deref()
.unwrap_or("none")
);
let tasks: Vec<TaskDefinition> = ffi_tasks
.iter()
.map(|t| TaskDefinition {
id: t.id.clone(),
function: "cloacina_execute_task".to_string(),
dependencies: t.dependencies.clone(),
description: if t.description.is_empty() {
None
} else {
Some(t.description.clone())
},
retries: 0,
timeout_seconds: None,
})
.collect();
let manifest = Manifest {
format_version: "2".to_string(),
package: PackageInfo {
name: package.name.clone(),
version: package.version.clone(),
description: package_metadata
.description
.or_else(|| Some(format!("Packaged workflow: {}", package.name))),
fingerprint,
targets: vec![target_platform],
},
language: PackageLanguage::Rust,
python: None,
rust: Some(RustRuntime {
library_path: library_filename,
}),
tasks,
triggers: vec![],
created_at: chrono::Utc::now(),
signature: None,
};
Ok(manifest)
}
#[derive(Debug, Clone)]
pub(crate) struct PackageMetadata {
pub description: Option<String>,
pub _author: Option<String>,
pub workflow_fingerprint: Option<String>,
}
#[derive(Debug, Clone)]
struct FfiTaskInfo {
pub _index: u32,
pub id: String,
pub dependencies: Vec<String>,
pub description: String,
pub _source_location: String,
}
fn extract_task_info_and_graph_from_library(
so_path: &Path,
project_path: &Path,
) -> Result<(
Vec<FfiTaskInfo>,
Option<crate::WorkflowGraphData>,
PackageMetadata,
)> {
let loaded = fidius_host::loader::load_library(so_path).with_context(|| {
format!(
"Failed to load plugin library for metadata extraction: {:?}",
so_path
)
})?;
let plugin = loaded
.plugins
.into_iter()
.next()
.ok_or_else(|| anyhow::anyhow!("Plugin library {:?} contains no plugins", so_path))?;
let handle = fidius_host::PluginHandle::from_loaded(plugin);
let meta: cloacina_workflow_plugin::PackageTasksMetadata = handle
.call_method(0, &())
.with_context(|| format!("Failed to call get_task_metadata on library {:?}", so_path))?;
let graph_data = if let Some(ref json) = meta.graph_data_json {
if json.trim().is_empty() {
None
} else {
Some(
serde_json::from_str::<crate::WorkflowGraphData>(json)
.map_err(|e| ManifestError::InvalidGraphData { source: e })
.map_err(|e| anyhow::anyhow!("{}", e))?,
)
}
} else {
None
};
let mut tasks = Vec::new();
for t in meta.tasks {
tasks.push(FfiTaskInfo {
_index: t.index,
id: t.id,
dependencies: t.dependencies,
description: t.description,
_source_location: t.source_location,
});
}
let package_metadata = PackageMetadata {
description: meta.package_description,
_author: meta.package_author,
workflow_fingerprint: meta.workflow_fingerprint,
};
let _ = project_path;
Ok((tasks, graph_data, package_metadata))
}
#[allow(dead_code)]
pub(crate) fn extract_package_names_from_source(project_path: &Path) -> Result<Vec<String>> {
let src_path = project_path.join("src");
let mut package_names = Vec::new();
for entry in std::fs::read_dir(&src_path)
.with_context(|| format!("Failed to read src directory: {:?}", src_path))?
{
let entry = entry?;
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) == Some("rs") {
let content = std::fs::read_to_string(&path)
.with_context(|| format!("Failed to read file: {:?}", path))?;
for captures in PACKAGED_WORKFLOW_REGEX.captures_iter(&content) {
if let Some(package_name) = captures.get(1) {
package_names.push(package_name.as_str().to_string());
}
}
}
}
Ok(package_names)
}
pub(crate) fn get_current_platform() -> String {
let os = std::env::consts::OS;
let arch = std::env::consts::ARCH;
let platform = match (os, arch) {
("macos", "aarch64") => "macos-arm64",
("macos", "x86_64") => "macos-x86_64",
("linux", "x86_64") => "linux-x86_64",
("linux", "aarch64") => "linux-arm64",
_ => return format!("{}-{}", os, arch),
};
platform.to_string()
}
#[allow(dead_code)]
pub(crate) fn get_current_architecture() -> String {
std::env::consts::ARCH.to_string()
}