diff --git a/e2e/config/test_tool_version_vars b/e2e/config/test_tool_version_vars new file mode 100755 index 0000000000..a903517dc1 --- /dev/null +++ b/e2e/config/test_tool_version_vars @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2016 + +# Test that env works with vars template (this already works) +cat <mise.toml +[env] +FOO = "{{vars.BAR}}" + +[vars] +BAR = "test" +EOF + +assert_contains "mise env -s bash | grep FOO" "export FOO=test" + +# Now test using vars in tool version +cat <mise.toml +[vars] +TINY_VERSION = "3.1.0" + +[tools] +tiny = "{{vars.TINY_VERSION}}" +EOF + +# Check that the tool request includes the expanded version +mise install tiny@3.1.0 >/dev/null 2>&1 || true +assert_contains "mise ls tiny" "3.1.0" + +# Test with more complex expression +cat <mise.toml +[vars] +NODE_MAJOR = "20" + +[tools] +node = "{{vars.NODE_MAJOR}}" +EOF + +# Just check that it tries to resolve it correctly +mise install node@20 >/dev/null 2>&1 || true +assert_contains "mise ls node" "20" + +# Test using env variables in tool versions (should already work) +cat <mise.toml +[tools] +tiny = "{{ env.MISE_TINY_VERSION | default(value='3.0.0') }}" +EOF + +MISE_TINY_VERSION=3.1.0 mise install >/dev/null 2>&1 || true +MISE_TINY_VERSION=3.1.0 assert_contains "mise ls tiny" "3.1.0" + +# Test vars that contain templates themselves +cat <mise.toml +[vars] +BASE_VERSION = "3" +TINY_VERSION = "{{vars.BASE_VERSION}}.1.0" + +[tools] +tiny = "{{vars.TINY_VERSION}}" +EOF + +# This should resolve to 3.1.0 +assert_contains "mise ls tiny" "3.1.0" + +# Test more complex nested templating +cat <mise.toml +[vars] +MAJOR = "20" +MINOR = "11" +NODE_VERSION = "{{vars.MAJOR}}.{{vars.MINOR}}.0" + +[tools] +node = "{{vars.NODE_VERSION}}" +EOF + +# This should resolve to 20.11.0 +assert_contains "mise ls node" "20.11.0" + +# Cleanup +rm -f mise.toml diff --git a/src/config/config_file/mise_toml.rs b/src/config/config_file/mise_toml.rs index a4730ca4af..9b6cda79a7 100644 --- a/src/config/config_file/mise_toml.rs +++ b/src/config/config_file/mise_toml.rs @@ -21,7 +21,7 @@ use crate::config::config_file::{ConfigFile, TaskConfig, config_trust_root, trus use crate::config::config_file::{config_root, toml::deserialize_arr}; use crate::config::env_directive::{EnvDirective, EnvDirectiveOptions}; use crate::config::settings::SettingsPartial; -use crate::config::{Alias, AliasMap}; +use crate::config::{Alias, AliasMap, Config}; use crate::file::{create_dir_all, display_path}; use crate::hooks::{Hook, Hooks}; use crate::redactions::Redactions; @@ -96,6 +96,10 @@ impl EnvList { } impl MiseToml { + fn contains_template_syntax(input: &str) -> bool { + input.contains("{{") || input.contains("{%") || input.contains("{#") + } + pub fn init(path: &Path) -> Self { let mut context = BASE_CONTEXT.clone(); context.insert( @@ -267,16 +271,22 @@ impl MiseToml { } fn parse_template(&self, input: &str) -> eyre::Result { - if !input.contains("{{") && !input.contains("{%") && !input.contains("{#") { + self.parse_template_with_context(&self.context, input) + } + + fn parse_template_with_context( + &self, + context: &TeraContext, + input: &str, + ) -> eyre::Result { + if !Self::contains_template_syntax(input) { return Ok(input.to_string()); } let dir = self.path.parent(); - let output = get_tera(dir) - .render_str(input, &self.context) - .wrap_err_with(|| { - let p = display_path(&self.path); - eyre!("failed to parse template {input} in {p}") - })?; + let output = get_tera(dir).render_str(input, context).wrap_err_with(|| { + let p = display_path(&self.path); + eyre!("failed to parse template {input} in {p}") + })?; Ok(output) } } @@ -484,12 +494,27 @@ impl ConfigFile for MiseToml { let source = ToolSource::MiseToml(self.path.clone()); let mut trs = ToolRequestSet::new(); let tools = self.tools.lock().unwrap(); + let mut context = self.context.clone(); + if context.get("vars").is_none() { + if let Some(config) = Config::maybe_get() { + if let Some(vars_results) = config.vars_results_cached() { + let vars = vars_results + .vars + .iter() + .map(|(k, (v, _))| (k.clone(), v.clone())) + .collect::>(); + context.insert("vars", &vars); + } else if !config.vars.is_empty() { + context.insert("vars", &config.vars); + } + } + } for (ba, tvp) in tools.iter() { for tool in &tvp.0 { - let version = self.parse_template(&tool.tt.to_string())?; + let version = self.parse_template_with_context(&context, &tool.tt.to_string())?; let tvr = if let Some(mut options) = tool.options.clone() { for v in options.opts.values_mut() { - *v = self.parse_template(v)?; + *v = self.parse_template_with_context(&context, v)?; } let mut ba = ba.clone(); let mut ba_opts = ba.opts().clone(); diff --git a/src/config/mod.rs b/src/config/mod.rs index 2882571b1e..20814a8cee 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -62,6 +62,8 @@ pub struct Config { tasks: OnceCell>, tool_request_set: OnceCell, toolset: OnceCell, + vars_loader: Option>, + vars_results: OnceCell, } #[derive(Debug, Clone, Default)] @@ -84,6 +86,9 @@ impl Config { } measure!("load config", { Self::load().await }) } + pub fn maybe_get() -> Option> { + _CONFIG.read().unwrap().as_ref().cloned() + } pub fn get_() -> Arc { (*_CONFIG.read().unwrap()).clone().unwrap() } @@ -140,6 +145,8 @@ impl Config { project_root: Default::default(), repo_urls: Default::default(), vars: Default::default(), + vars_loader: None, + vars_results: OnceCell::new(), }; let vars_config = Arc::new(Self { tera_ctx: config.tera_ctx.clone(), @@ -156,9 +163,15 @@ impl Config { project_root: config.project_root.clone(), repo_urls: config.repo_urls.clone(), vars: config.vars.clone(), + vars_loader: None, + vars_results: OnceCell::new(), }); let vars_results = measure!("config::load vars_results", { - load_vars(&vars_config).await? + let results = load_vars(&vars_config).await?; + vars_config.vars_results.set(results.clone()).ok(); + config.vars_results.set(results.clone()).ok(); + config.vars_loader = Some(vars_config.clone()); + results }); let vars: IndexMap = vars_results .vars @@ -263,6 +276,21 @@ impl Config { .get_or_try_init(|| async { self.load_env().await }) .await } + + pub async fn vars_results(self: &Arc) -> Result<&EnvResults> { + if let Some(loader) = &self.vars_loader { + if let Some(results) = loader.vars_results.get() { + return Ok(results); + } + } + self.vars_results + .get_or_try_init(|| async move { load_vars(self).await }) + .await + } + + pub fn vars_results_cached(&self) -> Option<&EnvResults> { + self.vars_results.get() + } pub async fn path_dirs(self: &Arc) -> eyre::Result<&Vec> { Ok(&self.env_results().await?.env_paths) }