use std::{collections::HashMap, path::PathBuf}; use anyhow::Context; use clap::ArgMatches; use rlua::Lua; use rlua_searcher::AddSearcher; use crate::{ actions::shell::ShellAction, model::{CuddleScript, CuddleShellScriptArg, CuddleVariable}, }; pub mod shell; #[derive(Debug, Clone)] #[allow(dead_code)] pub struct CuddleAction { pub script: CuddleScript, pub path: PathBuf, pub description: Option, pub name: String, } #[allow(dead_code)] impl CuddleAction { pub fn new( script: CuddleScript, path: PathBuf, name: String, description: Option, ) -> Self { Self { script, path, name, description, } } pub fn execute( self, matches: &ArgMatches, variables: Vec, ) -> anyhow::Result<()> { match self.script { CuddleScript::Shell(s) => { let mut arg_variables: Vec = vec![]; if let Some(args) = s.args { for (k, v) in args { let var = match v { CuddleShellScriptArg::Env(e) => { let env_var = matches.get_one::(&k).cloned().ok_or( anyhow::anyhow!( "failed to find env variable with key: {}", &e.key ), )?; CuddleVariable::new(k.clone(), env_var) } CuddleShellScriptArg::Flag(flag) => { match matches.get_one::(&flag.name) { Some(flag_var) => { CuddleVariable::new(k.clone(), flag_var.clone()) } None => continue, } } }; arg_variables.push(var); } } else { arg_variables = vec![] }; let mut vars = variables.clone(); vars.append(&mut arg_variables); log::trace!("preparing to run action"); match ShellAction::new( self.name.clone(), format!( "{}/scripts/{}.sh", self.path .to_str() .expect("action doesn't have a name, this should never happen"), self.name ), ) .execute(vars) { Ok(()) => { log::trace!("finished running action"); Ok(()) } Err(e) => { log::error!("{}", e); Err(e) } } } CuddleScript::Dagger(_d) => Err(anyhow::anyhow!("not implemented yet!")), CuddleScript::Lua(l) => { let lua = Lua::new(); let mut map = HashMap::new(); //map.insert("init".into(), "print(\"something\")".into()); let lua_dir = PathBuf::new().join(&self.path).join("scripts").join("lua"); if lua_dir.exists() { let absolute_lua_dir = lua_dir.canonicalize()?; for entry in walkdir::WalkDir::new(&lua_dir) .into_iter() .filter_map(|e| e.ok()) { if entry.metadata()?.is_file() { let full_file_path = entry.path().canonicalize()?; let relative_module_path = full_file_path.strip_prefix(&absolute_lua_dir)?; let module_path = relative_module_path .to_string_lossy() .to_string() .trim_end_matches("/init.lua") .trim_end_matches(".lua") .replace("/", "."); let contents = std::fs::read_to_string(entry.path())?; tracing::trace!(module_path = &module_path, "adding lua file"); map.insert(module_path.into(), contents.into()); } } } let lua_rocks_dir = PathBuf::new() .join(&self.path) .join("lua_modules") .join("share") .join("lua") .join("5.4"); if lua_rocks_dir.exists() { let absolute_lua_dir = lua_rocks_dir.canonicalize()?; for entry in walkdir::WalkDir::new(&lua_rocks_dir) .into_iter() .filter_map(|e| e.ok()) { if entry.metadata()?.is_file() { let full_file_path = entry.path().canonicalize()?; let relative_module_path = full_file_path.strip_prefix(&absolute_lua_dir)?; let module_path = relative_module_path .to_string_lossy() .to_string() .trim_end_matches("/init.lua") .trim_end_matches(".lua") .replace("/", "."); let contents = std::fs::read_to_string(entry.path())?; tracing::trace!(module_path = &module_path, "adding lua file"); map.insert(module_path.into(), contents.into()); } } } lua.context::<_, anyhow::Result<()>>(|lua_ctx| { lua_ctx.add_searcher(map)?; let globals = lua_ctx.globals(); let lua_script_entry = std::fs::read_to_string( PathBuf::new() .join(&self.path) .join("scripts") .join(format!("{}.lua", &self.name)), ) .context("failed to find lua script")?; lua_ctx .load(&lua_script_entry) .set_name(&self.name)? .exec()?; Ok(()) })?; Ok(()) } CuddleScript::Rust(script) => Ok(()), } } } pub mod rust_action { use std::{path::PathBuf, time::Duration}; use anyhow::Context; use futures_util::StreamExt; use reqwest::Method; use tokio::{fs::File, io::AsyncWriteExt}; use crate::model::{CuddleRustScript, CuddleRustUpstream, CuddleVariable}; pub struct RustActionConfig { pub config_dir: PathBuf, pub cache_dir: PathBuf, } impl Default for RustActionConfig { fn default() -> Self { let config = dirs::config_dir().expect("to be able to find a valid .config dir"); let cache = dirs::cache_dir().expect("to be able to find a valid .cache dir"); Self { config_dir: config, cache_dir: cache, } } } pub struct RustAction { pub config: RustActionConfig, pub plan: String, pub binary_name: String, } impl RustAction { pub fn new(plan: String, binary_name: String) -> Self { Self { plan, binary_name, config: RustActionConfig::default(), } } pub async fn execute( &self, script: CuddleRustScript, variables: impl IntoIterator, ) -> anyhow::Result<()> { let commit_sha = self .get_commit_sha() .await .context("failed to find a valid commit sha on the inferred path: .cuddle/plan")?; let binary_hash = self.calculate_hash(commit_sha)?; // Get cached binary // let binary = match self.get_binary(&binary_hash).await? { // Some(binary) => binary, // None => self.fetch_binary(&script, &binary_hash).await?, // }; // Execute binary Ok(()) } async fn get_binary( &self, binary_hash: impl Into, ) -> anyhow::Result> { let binary_path = self.get_cached_binary_path(binary_hash); if !binary_path.exists() { return Ok(None); } Ok(Some(RustBinary {})) } fn get_cached_binary_path(&self, binary_hash: impl Into) -> PathBuf { let cached_binary_name = self.get_cached_binary_name(binary_hash); let binary_path = self .config .cache_dir .join("binaries") .join(cached_binary_name); binary_path } #[inline] fn get_cached_binary_name(&self, binary_hash: impl Into) -> String { format!("{}-{}", binary_hash.into(), self.binary_name) } async fn get_commit_sha(&self) -> anyhow::Result { let repo = git2::Repository::open(".cuddle/plan")?; let head = repo.head()?; let commit = head.peel_to_commit()?; let commit_sha = commit.id(); Ok(commit_sha.to_string()) } // async fn fetch_binary( // &self, // script: &CuddleRustScript, // binary_hash: impl Into, // ) -> anyhow::Result { //let upstream = &script.upstream; //TODO: we should interpret some template variables in the upstream string. Ignore for now though // match UpstreamRustBinary::from(upstream) { // UpstreamRustBinary::HttpBased { url } => { // let client = reqwest::ClientBuilder::new() // .user_agent(concat!( // env!("CARGO_PKG_NAME"), // "/", // env!("CARGO_PKG_VERSION") // )) // .connect_timeout(Duration::from_secs(5)) // .build()?; // let resp = client.request(Method::GET, url).send().await?; // let mut stream = resp.bytes_stream(); // let mut file = File::create(self.get_cached_binary_name(binary_hash)).await?; // while let Some(item) = stream.next().await { // let chunk = item?; // file.write_all(&chunk).await?; // } // // Make sure the entire file is written before we execute it // file.flush().await?; // todo!() // } // } // } fn calculate_hash(&self, commit_sha: impl Into>) -> anyhow::Result { let mut contents: Vec = Vec::new(); contents.append(&mut self.plan.clone().into_bytes()); contents.append(&mut commit_sha.into()); let hash = blake3::hash(&contents); let hex = hash.to_hex(); Ok(hex.to_string()) } } pub struct RustBinary {} pub enum UpstreamRustBinary { HttpBased { url: String }, } impl From for UpstreamRustBinary { fn from(value: CuddleRustUpstream) -> Self { match value { CuddleRustUpstream::Gitea { url } => Self::HttpBased { url }, } } } impl From<&CuddleRustUpstream> for UpstreamRustBinary { fn from(value: &CuddleRustUpstream) -> Self { match value { CuddleRustUpstream::Gitea { url } => Self::HttpBased { url: url.clone() }, } } } }