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 Cargo.lock

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

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ glob = "0.3"
globset = "0.4"
ignore = { version = "0.4", features = [] }
heck = "0.5"
hex = "0.4"
humansize = "2"
indenter = "0.3"
indexmap = { version = "2", features = ["serde"] }
Expand Down Expand Up @@ -149,6 +150,7 @@ serde_yaml = "0.9"
sha1 = "0.10"
sha2 = "0.10"
blake3 = "1"
chacha20poly1305 = "0.10"

Copilot AI Jan 19, 2026

Copy link

Choose a reason for hiding this comment

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

The version 0.10 of chacha20poly1305 may not be the latest. Consider using a more recent version to benefit from security updates and improvements. Check crates.io for the latest stable version.

Suggested change
chacha20poly1305 = "0.10"
chacha20poly1305 = "0.10.1"

Copilot uses AI. Check for mistakes.
shell-escape = "0.1"
shell-words = "1"
signal-hook = "0.3"
Expand Down
93 changes: 89 additions & 4 deletions crates/vfox/src/hooks/mise_env.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use mlua::{IntoLua, Lua, LuaSerdeExt, Value};
use mlua::prelude::LuaError;
use mlua::{FromLua, IntoLua, Lua, LuaSerdeExt, Value};
use std::path::PathBuf;

use crate::Plugin;
use crate::error::Result;
Expand All @@ -10,20 +12,34 @@ pub struct MiseEnvContext<T: serde::Serialize> {
pub options: T,
}

/// Result from a mise_env hook call
/// Supports both legacy format (just array of env keys) and extended format
/// with cache metadata
#[derive(Debug, Default)]
pub struct MiseEnvResult {
/// Environment variables to set
pub env: Vec<EnvKey>,
/// Whether this module's output can be cached
/// Defaults to false for backward compatibility
pub cacheable: bool,
/// Files to watch for cache invalidation
pub watch_files: Vec<PathBuf>,
}

impl Plugin {
pub async fn mise_env<T: serde::Serialize>(
&self,
ctx: MiseEnvContext<T>,
) -> Result<Vec<EnvKey>> {
) -> Result<MiseEnvResult> {
debug!("[vfox:{}] mise_env", &self.name);
let env_keys = self
let result = self
.eval_async(chunk! {
require "hooks/mise_env"
return PLUGIN:MiseEnv($ctx)
})
.await?;

Ok(env_keys)
Ok(result)
}
}

Expand All @@ -34,3 +50,72 @@ impl<T: serde::Serialize> IntoLua for MiseEnvContext<T> {
Ok(Value::Table(table))
}
}

impl FromLua for MiseEnvResult {
fn from_lua(value: Value, lua: &Lua) -> std::result::Result<Self, LuaError> {
match value {
// Extended format: { cacheable = true, watch_files = {...}, env = {...} }
Value::Table(table) => {
// Check if this is extended format by looking for 'env' or 'cacheable' key
let has_env = table.contains_key("env")?;
let has_cacheable = table.contains_key("cacheable")?;
let has_watch_files = table.contains_key("watch_files")?;

if has_env || has_cacheable || has_watch_files {
// Extended format
let env: Vec<EnvKey> = table
.get::<Option<Vec<EnvKey>>>("env")
.map_err(|e| {
LuaError::RuntimeError(format!(
"Invalid 'env' field in MiseEnv result: expected array of {{key, value}} pairs. Error: {e}"
))
})?
.unwrap_or_default();
let cacheable: bool = table
.get::<Option<bool>>("cacheable")
.map_err(|e| {
LuaError::RuntimeError(format!(
"Invalid 'cacheable' field in MiseEnv result: expected boolean. Error: {e}"
))
})?
.unwrap_or(false);
let watch_files: Vec<String> = table
.get::<Option<Vec<String>>>("watch_files")
.map_err(|e| {
LuaError::RuntimeError(format!(
"Invalid 'watch_files' field in MiseEnv result: expected array of strings. Error: {e}"
))
})?
.unwrap_or_default();

Ok(MiseEnvResult {
env,
cacheable,
watch_files: watch_files.into_iter().map(PathBuf::from).collect(),
})
} else {
// Legacy format: table is actually an array of env keys
// Try to parse as array
let env: Vec<EnvKey> = Vec::from_lua(Value::Table(table), lua).map_err(|e| {
LuaError::RuntimeError(format!(
"Failed to parse MiseEnv hook result. Expected either:\n\
- Legacy format: array of {{key, value}} pairs like {{{{\"KEY\", \"VALUE\"}}, ...}}\n\
- Extended format: table with 'env' field like {{env = {{}}, cacheable = true}}\n\
Error: {e}"
))
})?;
Ok(MiseEnvResult {
env,
cacheable: false,
watch_files: vec![],
})
}
}
// Empty/nil result
Value::Nil => Ok(MiseEnvResult::default()),
_ => Err(LuaError::RuntimeError(
"Expected table or nil from MiseEnv hook".to_string(),
)),
}
}
}
6 changes: 3 additions & 3 deletions crates/vfox/src/vfox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use crate::hooks::backend_exec_env::BackendExecEnvContext;
use crate::hooks::backend_install::BackendInstallContext;
use crate::hooks::backend_list_versions::BackendListVersionsContext;
use crate::hooks::env_keys::{EnvKey, EnvKeysContext};
use crate::hooks::mise_env::MiseEnvContext;
use crate::hooks::mise_env::{MiseEnvContext, MiseEnvResult};
use crate::hooks::mise_path::MisePathContext;
use crate::hooks::parse_legacy_file::ParseLegacyFileResponse;
use crate::hooks::post_install::PostInstallContext;
Expand Down Expand Up @@ -229,10 +229,10 @@ impl Vfox {
sdk.env_keys(ctx).await
}

pub async fn mise_env<T: serde::Serialize>(&self, sdk: &str, opts: T) -> Result<Vec<EnvKey>> {
pub async fn mise_env<T: serde::Serialize>(&self, sdk: &str, opts: T) -> Result<MiseEnvResult> {
let plugin = self.get_sdk(sdk)?;
if !plugin.get_metadata()?.hooks.contains("mise_env") {
return Ok(vec![]);
return Ok(MiseEnvResult::default());
}
let ctx = MiseEnvContext {
args: vec![],
Expand Down
4 changes: 4 additions & 0 deletions docs/cli/exec.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ Command string to execute
Number of jobs to run in parallel
[default: 4]

### `--fresh-env`

Bypass the environment cache and recompute the environment

### `--no-prepare`

Skip automatic dependency preparation
Expand Down
4 changes: 4 additions & 0 deletions docs/cli/run.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ Don't show any output except for errors

Tool(s) to run in addition to what is in mise.toml files e.g.: node@20 python@3.10

### `--fresh-env`

Bypass the environment cache and recompute the environment

### `--no-cache`

Do not use cache on remote tasks
Expand Down
4 changes: 4 additions & 0 deletions docs/cli/tasks/run.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ Don't show any output except for errors

Tool(s) to run in addition to what is in mise.toml files e.g.: node@20 python@3.10

### `--fresh-env`

Bypass the environment cache and recompute the environment

### `--no-cache`

Do not use cache on remote tasks
Expand Down
61 changes: 49 additions & 12 deletions docs/env-plugin-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,43 @@ function PLUGIN:MiseEnv(ctx)
end
```

**Return value**: Array of tables, each with:
**Return value**: Either a simple array of env keys, or a table with caching metadata.

Simple format - array of tables, each with:

- `key` (string, required): Environment variable name
- `value` (string, required): Environment variable value

Extended format - table with:

- `env` (array, required): Array of `{key, value}` tables (same as simple format)
- `cacheable` (boolean, optional): If `true`, mise can cache this plugin's output. Default: `false`
- `watch_files` (array of strings, optional): File paths to watch for changes. If any file's mtime changes, the cache is invalidated.

Example using extended format with caching:

```lua
function PLUGIN:MiseEnv(ctx)
local config_path = ctx.options.config_file or "config.json"
local config = load_config(config_path)

return {
cacheable = true,
watch_files = {config_path},
env = {
{key = "API_URL", value = config.api_url},
{key = "API_KEY", value = config.api_key}
}
}
end
```

When `cacheable = true`, mise will cache the environment variables and only re-execute the plugin when:

- Any file in `watch_files` changes
- The mise configuration changes
- The cache TTL expires (configured via `env_cache_ttl` setting)

### hooks/mise_path.lua

The `MisePath` hook returns directories to add to PATH (optional):
Expand Down Expand Up @@ -256,27 +288,32 @@ function PLUGIN:MiseEnv(ctx)
end
```

### 4. Cache Expensive Operations
### 4. Use Built-in Caching for Expensive Operations

For plugins that fetch data from external services, consider caching:
For plugins that fetch data from external services, use mise's built-in caching by returning the extended format with `cacheable = true`:

```lua
local cache_file = os.getenv("HOME") .. "/.cache/my-plugin/secrets.json"

function PLUGIN:MiseEnv(ctx)
-- Check if cache is fresh
if is_cache_valid(cache_file, 300) then -- 5 minute cache
return load_from_cache(cache_file)
end
local config_file = ctx.options.config_file or "secrets.json"

-- Fetch fresh data
-- Fetch secrets (mise will cache the result)
local secrets = fetch_secrets(ctx.options)
save_to_cache(cache_file, secrets)

return secrets
return {
cacheable = true,
watch_files = {config_file}, -- Re-fetch if config changes
env = secrets
}
end
```

This is preferred over manual caching because:

- mise handles cache invalidation automatically
- Cache is encrypted with session-scoped keys
- Integrates with `mise cache clear` and `mise cache prune`
- Respects the `env_cache_ttl` setting

### 5. Support Multiple Environments

```lua
Expand Down
42 changes: 42 additions & 0 deletions e2e/env/test_env_cache
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#!/usr/bin/env bash

# Test env caching feature

# Enable env caching with encryption key (as would be set by `mise activate`)
export MISE_ENV_CACHE=1
export __MISE_ENV_CACHE_KEY="dGVzdGtleXRlc3RrZXl0ZXN0a2V5dGVzdGtleXRlc3Q="

# Create a basic config with env vars
cat >"$MISE_CONFIG_DIR/config.toml" <<EOF
[env]
CACHE_TEST_VAR="value1"
EOF

# First run should compute env and create cache
assert_contains "mise env -s bash" "CACHE_TEST_VAR=value1"

# Second run should use cached env (same result)
assert_contains "mise env -s bash" "CACHE_TEST_VAR=value1"

# Modify config - cache should be invalidated due to mtime change
sleep 1 # Ensure mtime is different
cat >"$MISE_CONFIG_DIR/config.toml" <<EOF
[env]
CACHE_TEST_VAR="value2"
EOF

# Should reflect the new value
assert_contains "mise env -s bash" "CACHE_TEST_VAR=value2"

# Test that cache dir exists when caching is enabled
assert_directory_exists "$HOME/.local/state/mise/env-cache"

# Disable caching and verify it still works
unset MISE_ENV_CACHE

cat >"$MISE_CONFIG_DIR/config.toml" <<EOF
[env]
CACHE_TEST_VAR="value3"
EOF

assert_contains "mise env -s bash" "CACHE_TEST_VAR=value3"
34 changes: 34 additions & 0 deletions e2e/env/test_env_cache_fresh
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/usr/bin/env bash

# Test --fresh-env flag with caching

# Enable env caching
export MISE_ENV_CACHE=1

# Set up a cache encryption key as if activated
export __MISE_ENV_CACHE_KEY="dGVzdGtleXRlc3RrZXl0ZXN0a2V5dGVzdGtleXRlc3Q="

# Create config
cat >"$MISE_CONFIG_DIR/config.toml" <<EOF
[env]
FRESH_TEST_VAR="original"
EOF

# First run caches
mise exec -- true

# Modify config
sleep 1
cat >"$MISE_CONFIG_DIR/config.toml" <<EOF
[env]
FRESH_TEST_VAR="modified"
EOF

# Normal exec should see cache invalidated due to mtime change
assert_contains "mise exec -- bash -c 'echo \$FRESH_TEST_VAR'" "modified"

# Verify --fresh-env flag exists on exec
assert_contains "mise exec --help" "fresh-env"

# Verify --fresh-env flag exists on run
assert_contains "mise run --help" "fresh-env"
Loading
Loading