use std::{ collections::BTreeMap, env::temp_dir, path::{Path, PathBuf}, }; use cuddle_actions_api::{ExecutableAction, ExecutableActions}; use cuddle_lazy::LazyResolve; use serde::Deserialize; pub struct RustActionsBuilder { root_path: PathBuf, } impl RustActionsBuilder { pub fn new(root_path: PathBuf) -> Self { Self { root_path } } fn actions_path(&self) -> Option { let actions_path = self.root_path.join("actions/rust"); if !actions_path.exists() { return None; } Some(actions_path) } fn global_registry(&self) -> anyhow::Result> { if let Some(dir) = dirs::cache_dir().map(|c| c.join("sh.cuddle/registry/actions/rust")) { if !dir.exists() { std::fs::create_dir_all(&dir)?; } Ok(Some(dir)) } else { Ok(None) } } pub async fn build(&self) -> anyhow::Result { tracing::debug!("building rust action: {}", self.root_path.display()); let Some(path) = self.actions_path() else { anyhow::bail!( "action was not found: {}", self.root_path.display().to_string() ); }; let actions_registry = self .global_registry()? .ok_or(anyhow::anyhow!("failed to find global registry"))?; let staging_id = uuid::Uuid::new_v4(); let actions_temp = temp_dir() .join("cuddle/actions") .join(staging_id.to_string()); std::fs::create_dir_all(&actions_temp)?; let actions_temp: TempGuard = actions_temp.into(); let mut hasher = blake3::Hasher::new(); tracing::debug!("moving file into: {}", actions_temp.display()); for entry in walkdir::WalkDir::new(&path) { let entry = entry?; let full_path = entry.path(); let rel_path = full_path .strip_prefix(path.canonicalize()?.to_string_lossy().to_string())? .to_string_lossy(); if rel_path.contains("target/") || rel_path.contains(".cuddle/") || rel_path.contains(".git/") { continue; } let metadata = entry.metadata()?; if metadata.is_file() { let temp_file_path = actions_temp.join(rel_path.to_string()); if let Some(temp_parent) = temp_file_path.parent() { std::fs::create_dir_all(temp_parent)?; } std::fs::copy(entry.path(), temp_file_path)?; hasher.update(rel_path.as_bytes()); let file_bytes = tokio::fs::read(entry.path()).await?; hasher.update(&file_bytes); } } let digest = hasher.finalize().to_hex().to_string(); let action_index = actions_registry.join(digest); if action_index.exists() { tracing::debug!("action already exists in: {}", action_index.display()); return self.get_actions(&action_index.join("action")).await; } std::fs::create_dir_all(&action_index)?; tracing::debug!("building rust code: {}", actions_temp.display()); let mut cmd = tokio::process::Command::new("cargo"); let output = cmd .args(vec!["build", "--release"]) .current_dir(actions_temp.as_path()) .output() .await?; if !output.status.success() { anyhow::bail!( "cargo build failed: {}", std::str::from_utf8(&output.stderr)? ); } let temp_file_bin_path = actions_temp.join("target/release/action"); tokio::fs::copy(temp_file_bin_path, action_index.join("action")).await?; self.get_actions(&action_index.join("action")).await } pub async fn get_actions(&self, action_path: &Path) -> anyhow::Result { tracing::debug!("querying schema: {}", action_path.display()); let output = tokio::process::Command::new(action_path.to_string_lossy().to_string()) .arg("schema") .output() .await?; let actions: CuddleActionsSchema = match serde_json::from_slice(&output.stdout) { Ok(output) => output, Err(e) => { let schema_output = std::str::from_utf8(&output.stdout)?; anyhow::bail!( "failed to query schema: {} {}", e.to_string(), schema_output ) } }; actions.to_executable(action_path) } } struct TempGuard(PathBuf); impl From for TempGuard { fn from(value: PathBuf) -> Self { Self(value) } } impl std::ops::Deref for TempGuard { type Target = PathBuf; fn deref(&self) -> &Self::Target { &self.0 } } impl Drop for TempGuard { fn drop(&mut self) { match std::fs::remove_dir_all(&self.0) { Ok(_) => { tracing::trace!("cleaned up temp dir: {}", self.0.display()); } Err(e) => panic!("{}", e), } } } #[derive(Debug, Deserialize, Clone)] struct CuddleActionsSchema { actions: Vec, } #[derive(Debug, Deserialize, Clone)] struct CuddleActionSchema { name: String, } impl CuddleActionsSchema { fn to_executable(&self, action_path: &Path) -> anyhow::Result { Ok(ExecutableActions { actions: self .actions .iter() .cloned() .map(|a| { let name = a.name.clone(); let action_path = action_path.to_string_lossy().to_string(); ExecutableAction { name: a.name, description: String::new(), flags: BTreeMap::default(), call_fn: LazyResolve::new(Box::new(move || { let name = name.clone(); let action_path = action_path.clone(); Box::pin(async move { if let Some(parent) = PathBuf::from(&action_path).parent() { tokio::process::Command::new("touch") .arg(parent) .output() .await?; } tracing::debug!("calling: {}", name); let mut cmd = tokio::process::Command::new(action_path); cmd.args(["do", &name]); let output = cmd.output().await?; let stdout = std::str::from_utf8(&output.stdout)?; for line in stdout.lines() { println!("{}: {}", &name, line); } tracing::debug!("finished call for output: {}", &name); Ok(()) }) })), } }) .collect(), }) } }