From 48c95c6527d231036d26f4114a6e9d1cb652132d Mon Sep 17 00:00:00 2001 From: jdx <216188+jdx@users.noreply.github.com> Date: Mon, 16 Dec 2024 11:27:21 -0600 Subject: [PATCH] feat: added cache feature to templates (#3608) Fixes #1261 --- docs/templates.md | 12 +++++ e2e/env/test_env_tmpl_cache | 30 +++++++++++++ src/config/env_directive/mod.rs | 33 +++++--------- src/tera.rs | 78 ++++++++++++++++++++++++++------- 4 files changed, 113 insertions(+), 40 deletions(-) create mode 100644 e2e/env/test_env_tmpl_cache diff --git a/docs/templates.md b/docs/templates.md index 0e23af4734..79f09bc7d0 100644 --- a/docs/templates.md +++ b/docs/templates.md @@ -164,6 +164,18 @@ An example of function using `exec`: current = "{{ exec(command='node --version') }}" ``` +### Exec Options + +The `exec` function supports the following options: + +- `command: String` – [required] The command to run. +- `cache_key: String` – The cache key to store the result. + If the cache key is provided, the result will be cached and reused + for subsequent calls. +- `cache_duration: String` – The duration to cache the result. + The duration is in seconds, minutes, hours, days, or weeks. + e.g. `cache_duration="1d"` will cache the result for 1 day. + ### Filters Tera offers many [built-in filters](https://keats.github.io/tera/docs/#built-in-filters). diff --git a/e2e/env/test_env_tmpl_cache b/e2e/env/test_env_tmpl_cache new file mode 100644 index 0000000000..9424fc6dc8 --- /dev/null +++ b/e2e/env/test_env_tmpl_cache @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +cat <<'EOF' >mise.toml +[env] +NOW="{{ exec(command='date') }}" +EOF + +now=$(mise env --json | jq -r '.NOW') +sleep 1 +assert_not_contains "mise env --json | jq -r '.NOW'" "$now" + +cat <<'EOF' >mise.toml +[env] +NOW="{{ exec(command='date', cache_key='now') }}" +EOF + +now=$(mise env --json | jq -r '.NOW') +sleep 1 +assert "mise env --json | jq -r '.NOW'" "$now" + +cat <<'EOF' >mise.toml +[env] +NOW="{{ exec(command='date', cache_duration='2s') }}" +EOF + +now=$(mise env --json | jq -r '.NOW') +sleep 1 +assert "mise env --json | jq -r '.NOW'" "$now" +sleep 1 +assert_not_contains "mise env --json | jq -r '.NOW'" "$now" diff --git a/src/config/env_directive/mod.rs b/src/config/env_directive/mod.rs index 48de446075..79a1912056 100644 --- a/src/config/env_directive/mod.rs +++ b/src/config/env_directive/mod.rs @@ -3,12 +3,11 @@ use std::collections::{BTreeSet, HashMap}; use std::fmt::{Debug, Display, Formatter}; use std::path::{Path, PathBuf}; -use crate::cmd::cmd; use crate::config::config_file::{config_root, trust_check}; use crate::dirs; use crate::env_diff::EnvMap; use crate::file::display_path; -use crate::tera::get_tera; +use crate::tera::{get_tera, tera_exec}; use eyre::{eyre, Context}; use indexmap::IndexMap; @@ -150,27 +149,15 @@ impl EnvResults { continue; } let mut tera = get_tera(source.parent()); - tera.register_function("exec", { - let source = source.clone(); - let env = env.clone(); - move |args: &HashMap| -> tera::Result { - match args.get("command") { - Some(tera::Value::String(command)) => { - let env = env::PRISTINE_ENV - .iter() - .map(|(k, v)| (k.clone(), v.clone())) - .chain(env.iter().map(|(k, (v, _))| (k.to_string(), v.to_string()))) - .collect::(); - let result = cmd("bash", ["-c", command]) - .full_env(&env) - .dir(config_root(&source)) - .read()?; - Ok(tera::Value::String(result)) - } - _ => Err("exec command must be a string".into()), - } - } - }); + tera.register_function( + "exec", + tera_exec( + source.parent().map(|d| d.to_path_buf()), + env.iter() + .map(|(k, (v, _))| (k.clone(), v.clone())) + .collect(), + ), + ); // trace!( // "resolve: directive: {:?}, source: {:?}", // &directive, diff --git a/src/tera.rs b/src/tera.rs index e688861731..e75240be17 100644 --- a/src/tera.rs +++ b/src/tera.rs @@ -11,8 +11,10 @@ use rand::thread_rng; use tera::{Context, Tera, Value}; use versions::{Requirement, Versioning}; +use crate::cache::CacheManagerBuilder; use crate::cmd::cmd; -use crate::{env, hash}; +use crate::env_diff::EnvMap; +use crate::{dirs, env, hash}; pub static BASE_CONTEXT: Lazy = Lazy::new(|| { let mut context = Context::new(); @@ -297,26 +299,68 @@ static TERA: Lazy = Lazy::new(|| { pub fn get_tera(dir: Option<&Path>) -> Tera { let mut tera = TERA.clone(); let dir = dir.map(PathBuf::from); - tera.register_function( - "exec", - move |args: &HashMap| -> tera::Result { - match args.get("command") { - Some(Value::String(command)) => { - let mut cmd = cmd("bash", ["-c", command]).full_env(&*env::PRISTINE_ENV); - if let Some(dir) = &dir { - cmd = cmd.dir(dir); - } - let result = cmd.read()?; - Ok(Value::String(result)) - } - _ => Err("exec command must be a string".into()), - } - }, - ); + tera.register_function("exec", tera_exec(dir, env::PRISTINE_ENV.clone())); tera } +pub fn tera_exec( + dir: Option, + env: EnvMap, +) -> impl Fn(&HashMap) -> tera::Result { + move |args: &HashMap| -> tera::Result { + let cache = match args.get("cache_key") { + Some(Value::String(cache)) => Some(cache), + None => None, + _ => return Err("exec cache_key must be a string".into()), + }; + let cache_duration = match args.get("cache_duration") { + Some(Value::String(duration)) => match humantime::parse_duration(&duration.to_string()) + { + Ok(duration) => Some(duration), + Err(e) => return Err(format!("exec cache_duration: {}", e).into()), + }, + None => None, + _ => return Err("exec cache_duration must be an integer".into()), + }; + match args.get("command") { + Some(Value::String(command)) => { + let mut cmd = cmd("bash", ["-c", command]).full_env(&env); + if let Some(dir) = &dir { + cmd = cmd.dir(dir); + } + let result = if cache.is_some() || cache_duration.is_some() { + let cachehash = hash::hash_sha256_to_str( + &(dir + .as_ref() + .map(|d| d.to_string_lossy().to_string()) + .unwrap_or_default() + + command), + )[..8] + .to_string(); + let mut cacheman = + CacheManagerBuilder::new(dirs::CACHE.join("exec").join(cachehash)); + if let Some(cache) = cache { + cacheman = cacheman.with_cache_key(cache.clone()); + } + if let Some(cache_duration) = cache_duration { + cacheman = cacheman.with_fresh_duration(Some(cache_duration)); + } + let cache = cacheman.build(); + match cache.get_or_try_init(|| Ok(cmd.read()?)) { + Ok(result) => result.clone(), + Err(e) => return Err(format!("exec command: {}", e).into()), + } + } else { + cmd.read()? + }; + Ok(Value::String(result)) + } + _ => Err("exec command must be a string".into()), + } + } +} + #[cfg(test)] mod tests { use super::*;