Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions e2e/config/test_tool_version_vars
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#!/usr/bin/env bash
# shellcheck disable=SC2016

# Test that env works with vars template (this already works)
cat <<EOF >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 <<EOF >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 <<EOF >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 <<EOF >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 <<EOF >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 <<EOF >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
45 changes: 35 additions & 10 deletions src/config/config_file/mise_toml.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -267,16 +271,22 @@ impl MiseToml {
}

fn parse_template(&self, input: &str) -> eyre::Result<String> {
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<String> {
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)
}
}
Expand Down Expand Up @@ -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() {
Copy link

Copilot AI Sep 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The string literal "vars" is used multiple times in this method. Consider defining it as a constant to avoid magic strings and improve maintainability.

Copilot uses AI. Check for mistakes.
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::<IndexMap<_, _>>();
context.insert("vars", &vars);
Comment on lines +501 to +506
Copy link

Copilot AI Sep 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This creates unnecessary clones of all keys and values. Consider using references in the IndexMap or restructuring to avoid the clone overhead, especially if vars can be large.

Suggested change
let vars = vars_results
.vars
.iter()
.map(|(k, (v, _))| (k.clone(), v.clone()))
.collect::<IndexMap<_, _>>();
context.insert("vars", &vars);
// Avoid unnecessary clones by passing a reference to the existing vars map
context.insert("vars", &vars_results.vars);

Copilot uses AI. Check for mistakes.
} else if !config.vars.is_empty() {
Comment on lines +501 to +507
Copy link

Copilot AI Sep 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The vars collection is being cloned twice on each call (line 520). Consider caching this transformed vars map or using references where possible to avoid unnecessary allocations, especially if this method is called frequently during tool resolution.

Suggested change
let vars = vars_results
.vars
.iter()
.map(|(k, (v, _))| (k.clone(), v.clone()))
.collect::<IndexMap<_, _>>();
context.insert("vars", &vars);
} else if !config.vars.is_empty() {
// Cache the transformed vars map to avoid double cloning
let vars: IndexMap<_, _> = vars_results
.vars
.iter()
.map(|(k, (v, _))| (k.clone(), v.clone()))
.collect();
context.insert("vars", &vars);
} else if !config.vars.is_empty() {
// Insert a reference to the vars map directly to avoid unnecessary cloning

Copilot uses AI. Check for mistakes.
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();
Expand Down
30 changes: 29 additions & 1 deletion src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ pub struct Config {
tasks: OnceCell<BTreeMap<String, Task>>,
tool_request_set: OnceCell<ToolRequestSet>,
toolset: OnceCell<Toolset>,
vars_loader: Option<Arc<Config>>,
vars_results: OnceCell<EnvResults>,
}

#[derive(Debug, Clone, Default)]
Expand All @@ -84,6 +86,9 @@ impl Config {
}
measure!("load config", { Self::load().await })
}
pub fn maybe_get() -> Option<Arc<Self>> {
_CONFIG.read().unwrap().as_ref().cloned()
}
Comment on lines +89 to +91
Copy link

Copilot AI Sep 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The .unwrap() call will panic if the lock is poisoned. Consider using a more descriptive error message or handling the poison error gracefully with .expect("Config lock poisoned") or proper error handling.

Copilot uses AI. Check for mistakes.
Comment on lines +89 to +91
Copy link

Copilot AI Sep 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using expect() with a descriptive message instead of unwrap() to provide better error context if the lock is poisoned.

Copilot uses AI. Check for mistakes.
pub fn get_() -> Arc<Self> {
(*_CONFIG.read().unwrap()).clone().unwrap()
}
Expand Down Expand Up @@ -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(),
Expand All @@ -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<String, String> = vars_results
.vars
Expand Down Expand Up @@ -263,6 +276,21 @@ impl Config {
.get_or_try_init(|| async { self.load_env().await })
.await
}

pub async fn vars_results(self: &Arc<Self>) -> 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<Self>) -> eyre::Result<&Vec<PathBuf>> {
Ok(&self.env_results().await?.env_paths)
}
Expand Down
Loading