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
2 changes: 2 additions & 0 deletions crates/vfox/src/hooks/mise_env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use crate::hooks::env_keys::EnvKey;
pub struct MiseEnvContext<T: serde::Serialize> {
pub args: Vec<String>,
pub options: T,
pub config_root: Option<String>,
}

/// Result from a mise_env hook call
Expand Down Expand Up @@ -50,6 +51,7 @@ impl<T: serde::Serialize> IntoLua for MiseEnvContext<T> {
fn into_lua(self, lua: &Lua) -> mlua::Result<Value> {
let table = lua.create_table()?;
table.set("options", lua.to_value(&self.options)?)?;
table.set("config_root", self.config_root)?;
Ok(Value::Table(table))
}
}
Expand Down
2 changes: 2 additions & 0 deletions crates/vfox/src/hooks/mise_path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use crate::error::Result;
pub struct MisePathContext<T: serde::Serialize> {
pub args: Vec<String>,
pub options: T,
pub config_root: Option<String>,
}

impl Plugin {
Expand All @@ -30,6 +31,7 @@ impl<T: serde::Serialize> IntoLua for MisePathContext<T> {
fn into_lua(self, lua: &Lua) -> mlua::Result<Value> {
let table = lua.create_table()?;
table.set("options", lua.to_value(&self.options)?)?;
table.set("config_root", self.config_root)?;
Ok(Value::Table(table))
}
}
4 changes: 4 additions & 0 deletions crates/vfox/src/vfox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@ impl Vfox {
sdk: &str,
opts: T,
env: &indexmap::IndexMap<String, String>,
config_root: Option<&str>,
) -> Result<MiseEnvResult> {
let plugin = self.get_sdk(sdk)?;
if !plugin.get_metadata()?.hooks.contains("mise_env") {
Expand All @@ -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
}
Expand Down Expand Up @@ -381,6 +383,7 @@ impl Vfox {
sdk: &str,
opts: T,
env: &indexmap::IndexMap<String, String>,
config_root: Option<&str>,
) -> Result<Vec<String>> {
let plugin = self.get_sdk(sdk)?;
if !plugin.get_metadata()?.hooks.contains("mise_path") {
Expand All @@ -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
}
Expand Down
75 changes: 75 additions & 0 deletions e2e/env/test_env_module_config_root
Original file line number Diff line number Diff line change
@@ -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"
2 changes: 2 additions & 0 deletions mise.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 5 additions & 6 deletions src/config/env_directive/module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,13 @@ impl EnvResults {
redact: Option<bool>,
env: IndexMap<String, String>,
) -> 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;
Expand All @@ -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));
}
}

Expand All @@ -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());
}
Expand Down
12 changes: 9 additions & 3 deletions src/plugins/vfox_plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -82,9 +82,12 @@ impl VfoxPlugin {
&self,
opts: &toml::Value,
env: &IndexMap<String, String>,
config_root: Option<&Path>,
) -> Result<Option<MiseEnvResponse>> {
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);
Expand All @@ -101,10 +104,13 @@ impl VfoxPlugin {
&self,
opts: &toml::Value,
env: &IndexMap<String, String>,
config_root: Option<&Path>,
) -> Result<Option<Vec<String>>> {
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);
}
Expand Down
Loading