diff --git a/crates/vfox/src/hooks/mise_env.rs b/crates/vfox/src/hooks/mise_env.rs index 4fb8b645ae..d9a8fa04ce 100644 --- a/crates/vfox/src/hooks/mise_env.rs +++ b/crates/vfox/src/hooks/mise_env.rs @@ -10,6 +10,7 @@ use crate::hooks::env_keys::EnvKey; pub struct MiseEnvContext { pub args: Vec, pub options: T, + pub config_root: Option, } /// Result from a mise_env hook call @@ -50,6 +51,7 @@ impl IntoLua for MiseEnvContext { fn into_lua(self, lua: &Lua) -> mlua::Result { let table = lua.create_table()?; table.set("options", lua.to_value(&self.options)?)?; + table.set("config_root", self.config_root)?; Ok(Value::Table(table)) } } diff --git a/crates/vfox/src/hooks/mise_path.rs b/crates/vfox/src/hooks/mise_path.rs index 4290501218..b776d8ed62 100644 --- a/crates/vfox/src/hooks/mise_path.rs +++ b/crates/vfox/src/hooks/mise_path.rs @@ -7,6 +7,7 @@ use crate::error::Result; pub struct MisePathContext { pub args: Vec, pub options: T, + pub config_root: Option, } impl Plugin { @@ -30,6 +31,7 @@ impl IntoLua for MisePathContext { fn into_lua(self, lua: &Lua) -> mlua::Result { let table = lua.create_table()?; table.set("options", lua.to_value(&self.options)?)?; + table.set("config_root", self.config_root)?; Ok(Value::Table(table)) } } diff --git a/crates/vfox/src/vfox.rs b/crates/vfox/src/vfox.rs index d806b6db4d..240bfcf77b 100644 --- a/crates/vfox/src/vfox.rs +++ b/crates/vfox/src/vfox.rs @@ -302,6 +302,7 @@ impl Vfox { sdk: &str, opts: T, env: &indexmap::IndexMap, + config_root: Option<&str>, ) -> Result { let plugin = self.get_sdk(sdk)?; if !plugin.get_metadata()?.hooks.contains("mise_env") { @@ -319,6 +320,7 @@ impl Vfox { let ctx = MiseEnvContext { args: vec![], options: opts, + config_root: config_root.map(|s| s.to_string()), }; plugin.mise_env(ctx).await } @@ -381,6 +383,7 @@ impl Vfox { sdk: &str, opts: T, env: &indexmap::IndexMap, + config_root: Option<&str>, ) -> Result> { let plugin = self.get_sdk(sdk)?; if !plugin.get_metadata()?.hooks.contains("mise_path") { @@ -391,6 +394,7 @@ impl Vfox { let ctx = MisePathContext { args: vec![], options: opts, + config_root: config_root.map(|s| s.to_string()), }; plugin.mise_path(ctx).await } diff --git a/e2e/env/test_env_module_config_root b/e2e/env/test_env_module_config_root new file mode 100644 index 0000000000..cf65aebe94 --- /dev/null +++ b/e2e/env/test_env_module_config_root @@ -0,0 +1,75 @@ +#!/usr/bin/env bash + +# Test that env module plugins receive ctx.config_root pointing to the +# directory of the mise.toml that defines the module directive. +# config_root resolves to the project root (not the config file's directory), +# matching the behavior of built-in directives like _.file. + +# Create a vfox env plugin that returns config_root as an env var +PLUGIN_DIR="$MISE_DATA_DIR/plugins/test-config-root" +mkdir -p "$PLUGIN_DIR/hooks" + +cat >"$PLUGIN_DIR/metadata.lua" <<'EOFMETA' +PLUGIN = {} +PLUGIN.name = "test-config-root" +PLUGIN.version = "1.0.0" +PLUGIN.homepage = "https://example.com" +PLUGIN.license = "MIT" +PLUGIN.description = "Test plugin for config_root access" +PLUGIN.minRuntimeVersion = "0.3.0" +EOFMETA + +cat >"$PLUGIN_DIR/hooks/mise_env.lua" <<'EOFHOOK' +function PLUGIN:MiseEnv(ctx) + -- ctx.config_root should be the project root for the config file + local config_root = ctx.config_root or "NIL" + local version_file = ctx.options.version_file or "" + + -- Resolve relative path against config_root + local resolved = "" + if config_root ~= "NIL" and version_file ~= "" then + resolved = config_root .. "/" .. version_file + end + + return { + env = { + {key = "TEST_CONFIG_ROOT", value = config_root}, + {key = "TEST_RESOLVED_PATH", value = resolved} + }, + cacheable = false, + watch_files = {} + } +end +EOFHOOK + +# Test 1: config_root is set when using global config +# Global config at $MISE_CONFIG_DIR/config.toml -> config_root is $HOME +cat >"$MISE_CONFIG_DIR/config.toml" <<'EOF' +[env] +_.test-config-root = { version_file = ".xcode-version" } +EOF + +eval "$(mise env -s bash)" +assert "echo $TEST_CONFIG_ROOT" "$HOME" +assert "echo $TEST_RESOLVED_PATH" "$HOME/.xcode-version" + +# Test 2: project-level mise.toml uses its own directory as config_root +PROJECT_DIR="$HOME/myproject" +mkdir -p "$PROJECT_DIR" +cat >"$PROJECT_DIR/mise.toml" <<'EOF' +[env] +_.test-config-root = { version_file = ".config/.xcode-version" } +EOF + +cd "$PROJECT_DIR" +eval "$(mise env -s bash)" +assert "echo $TEST_CONFIG_ROOT" "$PROJECT_DIR" +assert "echo $TEST_RESOLVED_PATH" "$PROJECT_DIR/.config/.xcode-version" + +# Test 3: config_root is stable regardless of cwd +SUBDIR="$PROJECT_DIR/deep/sub" +mkdir -p "$SUBDIR" +cd "$SUBDIR" +eval "$(mise env -s bash)" +# Should still point to the project dir where mise.toml lives +assert "echo $TEST_CONFIG_ROOT" "$PROJECT_DIR" diff --git a/mise.lock b/mise.lock index ea1c7b8185..54592d6179 100644 --- a/mise.lock +++ b/mise.lock @@ -146,6 +146,7 @@ url_api = "https://api.github.com/repos/endevco/aube/releases/assets/404654425" checksum = "sha256:4d77b4f54c78297e6aef6da4ccb8a2327c31297f1a9f37b84fb746928129c4b2" url = "https://github.com/endevco/aube/releases/download/v1.1.0/aube-v1.1.0-aarch64-apple-darwin.tar.gz" url_api = "https://api.github.com/repos/endevco/aube/releases/assets/404655454" +provenance = "github-attestations" [tools.aube."platforms.windows-x64"] checksum = "sha256:41f7d31e35bf7e21d32221ad74fb535bca3fc2fde9da80aee6061cc061f5e0f7" @@ -315,6 +316,7 @@ url_api = "https://api.github.com/repos/jdx/communique/releases/assets/405964691 checksum = "sha256:459993e31a6c4ccbd09882f5679a2bc1ea5d9068701ecefc411a00fb69ce82e6" url = "https://github.com/jdx/communique/releases/download/v1.1.2/communique-aarch64-apple-darwin.tar.gz" url_api = "https://api.github.com/repos/jdx/communique/releases/assets/405964098" +provenance = "github-attestations" [tools.communique."platforms.windows-x64"] checksum = "sha256:3cc0e880ac2168aed3163223627bbd1eee62e07a9901cb85cb507c6c8927bc93" diff --git a/src/config/env_directive/module.rs b/src/config/env_directive/module.rs index 1920de2eba..842132da22 100644 --- a/src/config/env_directive/module.rs +++ b/src/config/env_directive/module.rs @@ -21,12 +21,13 @@ impl EnvResults { redact: Option, env: IndexMap, ) -> Result<()> { + let config_root = crate::config::config_file::config_root::config_root(&source); let path = dirs::PLUGINS.join(name.to_kebab_case()); let plugin = VfoxPlugin::new(name, path.clone()); plugin .ensure_installed(config, &MultiProgressReport::get(), false, false) .await?; - if let Some(response) = plugin.mise_env(value, &env).await? { + if let Some(response) = plugin.mise_env(value, &env, Some(&config_root)).await? { // Track cacheability if !response.cacheable { r.has_uncacheable = true; @@ -37,14 +38,12 @@ impl EnvResults { r.watch_files.push(path); // Add watch files for cache invalidation - // Absolutize relative paths to ensure consistent cache validation - // regardless of which directory mise is run from - let cwd = std::env::current_dir().unwrap_or_default(); + // Absolutize relative paths relative to config_root for consistent cache validation for watch_file in response.watch_files { if watch_file.is_absolute() { r.watch_files.push(watch_file); } else { - r.watch_files.push(cwd.join(watch_file)); + r.watch_files.push(config_root.join(watch_file)); } } @@ -58,7 +57,7 @@ impl EnvResults { r.env.insert(k, (v, source.clone())); } } - if let Some(path) = plugin.mise_path(value, &env).await? { + if let Some(path) = plugin.mise_path(value, &env, Some(&config_root)).await? { for p in path { r.env_paths.push(p.into()); } diff --git a/src/plugins/vfox_plugin.rs b/src/plugins/vfox_plugin.rs index 91da5e77e1..556e526829 100644 --- a/src/plugins/vfox_plugin.rs +++ b/src/plugins/vfox_plugin.rs @@ -15,7 +15,7 @@ use console::style; use contracts::requires; use eyre::{Context, bail, eyre}; use indexmap::{IndexMap, indexmap}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex, MutexGuard, mpsc}; use url::Url; use vfox::Vfox; @@ -82,9 +82,12 @@ impl VfoxPlugin { &self, opts: &toml::Value, env: &IndexMap, + config_root: Option<&Path>, ) -> Result> { let (vfox, _) = self.vfox(); - let result = vfox.mise_env(&self.name, opts, env).await?; + let result = vfox + .mise_env(&self.name, opts, env, config_root.and_then(|p| p.to_str())) + .await?; let mut result_env = indexmap!(); for ek in result.env { result_env.insert(ek.key, ek.value); @@ -101,10 +104,13 @@ impl VfoxPlugin { &self, opts: &toml::Value, env: &IndexMap, + config_root: Option<&Path>, ) -> Result>> { let (vfox, _) = self.vfox(); let mut out = vec![]; - let results = vfox.mise_path(&self.name, opts, env).await?; + let results = vfox + .mise_path(&self.name, opts, env, config_root.and_then(|p| p.to_str())) + .await?; for entry in results { out.push(entry); }