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
17 changes: 14 additions & 3 deletions docs/dev-tools/backend_architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,23 @@ Support for external plugin ecosystems:
When you specify a tool, mise determines the backend using this priority:

1. **Explicit backend**: `mise use aqua:golangci/golangci-lint`
2. **Registry lookup**: `mise use golangci-lint` → checks registry for default backend
3. **Core tools**: `mise use node` → uses built-in core backend
4. **Fallback**: If not found, suggests available backends
2. **Environment variable override**: `MISE_BACKENDS_<TOOL>` (see below)
3. **Registry lookup**: `mise use golangci-lint` → checks registry for default backend
4. **Core tools**: `mise use node` → uses built-in core backend
5. **Fallback**: If not found, suggests available backends

The [mise registry](../registry.md) defines a priority order for which backend to use for each tool, so typically end-users don't need to know which backend to choose unless they want tools not available in the registry or want to override the default selection.

### Environment Variable Overrides

You can override the backend for any tool using the `MISE_BACKENDS_<TOOL>` environment variable pattern. The tool name is converted to SHOUTY_SNAKE_CASE (uppercase with underscores replacing hyphens).

```bash
# Use vfox backend for php
export MISE_BACKENDS_PHP='vfox:mise-plugins/vfox-php'
mise install php@latest
```

### Registry System

The [registry](../registry.md) (`mise registry`) maps short names to full backend specifications with a preferred priority order:
Expand Down
12 changes: 12 additions & 0 deletions docs/registry.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,18 @@ This will disable the [asdf](./dev-tools/backends/asdf.html) backend. See [Alias

You can also specify the full name for a tool using `mise use aqua:1password/cli` if you want to use a specific backend.

### Environment Variable Overrides

You can override the backend for any tool using environment variables with the pattern `MISE_BACKENDS_<TOOL>`. This takes the highest priority and overrides any registry or alias configuration:

```shell
# Use vfox backend for php
export MISE_BACKENDS_PHP='vfox:mise-plugins/vfox-php'
mise install php@latest
```

The tool name in the environment variable should be in SHOUTY_SNAKE_CASE (uppercase with underscores). For example, `my-tool` becomes `MISE_BACKENDS_MY_TOOL`.

Source: <https://github.com/jdx/mise/blob/main/registry.toml>

## Tools {#tools}
Expand Down
10 changes: 10 additions & 0 deletions e2e/backend/test_backend_env_override
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/usr/bin/env bash
set -euo pipefail

# Test MISE_BACKENDS_* environment variable override for non-registry tools

# Test 1: Non-registry tool with custom backend via env var
echo "Testing non-registry tool with env override..."
export MISE_BACKENDS_JUST_TEST='github:casey/just[bin=just]'

assert "mise exec just-test@1.43.0 -- just --version" "just 1.43.0"
11 changes: 10 additions & 1 deletion src/cli/args/backend_arg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ use crate::toolset::{ToolVersionOptions, install_state, parse_tool_options};
use crate::{backend, config, dirs, lockfile, registry};
use contracts::requires;
use eyre::{Result, bail};
use heck::ToKebabCase;
use heck::{ToKebabCase, ToShoutySnakeCase};
use std::collections::HashSet;
use std::env;
use std::fmt::{Debug, Display};
use std::hash::Hash;
use std::path::PathBuf;
Expand Down Expand Up @@ -175,6 +176,14 @@ impl BackendArg {

pub fn full(&self) -> String {
let short = unalias_backend(&self.short);

// Check for environment variable override first
// e.g., MISE_BACKENDS_MYTOOLS='github:myorg/mytools'
let env_key = format!("MISE_BACKENDS_{}", short.to_shouty_snake_case());
if let Ok(env_value) = env::var(&env_key) {
return env_value;
}

if config::is_loaded() {
if let Some(full) = Config::get_()
.all_aliases
Expand Down
61 changes: 60 additions & 1 deletion src/registry.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
use crate::backend::backend_type::BackendType;
use crate::cli::args::BackendArg;
use crate::config::Settings;
use heck::ToShoutySnakeCase;
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use std::env;
use std::env::consts::{ARCH, OS};
use std::fmt::Display;
use std::iter::Iterator;
use std::sync::LazyLock as Lazy;
use std::sync::{LazyLock as Lazy, Mutex};
use strum::IntoEnumIterator;
use url::Url;

Expand All @@ -32,8 +34,33 @@ pub struct RegistryBackend {
pub platforms: &'static [&'static str],
}

// Cache for environment variable overrides
static ENV_BACKENDS: Lazy<Mutex<HashMap<String, &'static str>>> =
Lazy::new(|| Mutex::new(HashMap::new()));

impl RegistryTool {
pub fn backends(&self) -> Vec<&'static str> {
// Check for environment variable override first
// e.g., MISE_BACKENDS_GRAPHITE='github:withgraphite/homebrew-tap[exe=gt]'
let env_key = format!("MISE_BACKENDS_{}", self.short.to_shouty_snake_case());

// Check cache first
{
let cache = ENV_BACKENDS.lock().unwrap();
if let Some(&backend) = cache.get(&env_key) {
return vec![backend];
}
}

// Check environment variable
if let Ok(env_value) = env::var(&env_key) {
// Store in cache with 'static lifetime
let leaked = Box::leak(env_value.into_boxed_str());
let mut cache = ENV_BACKENDS.lock().unwrap();
cache.insert(env_key.clone(), leaked);
return vec![leaked];
}
Copy link

Choose a reason for hiding this comment

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

Bug: RegistryTool Memory Leak

Memory leak in RegistryTool::backends(). When an environment variable override is set for a tool, each call to backends() uses Box::leak() to convert the environment variable string to a &'static str. This permanently leaks the string's memory, leading to accumulated memory growth with repeated calls.

Fix in Cursor Fix in Web


static BACKEND_TYPES: Lazy<HashSet<String>> = Lazy::new(|| {
let mut backend_types = BackendType::iter()
.map(|b| b.to_string())
Expand Down Expand Up @@ -172,4 +199,36 @@ mod tests {
&name
));
}

#[tokio::test]
async fn test_backend_env_override() {
let _config = Config::get().await.unwrap();
use super::*;

// Clear the cache first
ENV_BACKENDS.lock().unwrap().clear();

// Test with a known tool from the registry
if let Some(tool) = REGISTRY.get("node") {
// First test without env var - should return default backends
let default_backends = tool.backends();
assert!(!default_backends.is_empty());

// Test with env var override
// SAFETY: This is safe in a test environment
unsafe {
env::set_var("MISE_BACKENDS_NODE", "test:backend");
}
let overridden_backends = tool.backends();
assert_eq!(overridden_backends.len(), 1);
assert_eq!(overridden_backends[0], "test:backend");

// Clean up
// SAFETY: This is safe in a test environment
unsafe {
env::remove_var("MISE_BACKENDS_NODE");
}
Comment on lines +218 to +230
Copy link

Copilot AI Sep 28, 2025

Choose a reason for hiding this comment

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

Using unsafe blocks for env::set_var is unnecessary as this function is not unsafe. The env::set_var and env::remove_var functions are safe to call and don't require unsafe blocks.

Suggested change
// SAFETY: This is safe in a test environment
unsafe {
env::set_var("MISE_BACKENDS_NODE", "test:backend");
}
let overridden_backends = tool.backends();
assert_eq!(overridden_backends.len(), 1);
assert_eq!(overridden_backends[0], "test:backend");
// Clean up
// SAFETY: This is safe in a test environment
unsafe {
env::remove_var("MISE_BACKENDS_NODE");
}
// Set environment variable for test
env::set_var("MISE_BACKENDS_NODE", "test:backend");
let overridden_backends = tool.backends();
assert_eq!(overridden_backends.len(), 1);
assert_eq!(overridden_backends[0], "test:backend");
// Clean up
// Clean up environment variable after test
env::remove_var("MISE_BACKENDS_NODE");

Copilot uses AI. Check for mistakes.
Comment on lines +218 to +230
Copy link

Copilot AI Sep 28, 2025

Choose a reason for hiding this comment

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

Using unsafe blocks for env::remove_var is unnecessary as this function is not unsafe. The env::set_var and env::remove_var functions are safe to call and don't require unsafe blocks.

Suggested change
// SAFETY: This is safe in a test environment
unsafe {
env::set_var("MISE_BACKENDS_NODE", "test:backend");
}
let overridden_backends = tool.backends();
assert_eq!(overridden_backends.len(), 1);
assert_eq!(overridden_backends[0], "test:backend");
// Clean up
// SAFETY: This is safe in a test environment
unsafe {
env::remove_var("MISE_BACKENDS_NODE");
}
env::set_var("MISE_BACKENDS_NODE", "test:backend");
let overridden_backends = tool.backends();
assert_eq!(overridden_backends.len(), 1);
assert_eq!(overridden_backends[0], "test:backend");
// Clean up
env::remove_var("MISE_BACKENDS_NODE");

Copilot uses AI. Check for mistakes.
ENV_BACKENDS.lock().unwrap().clear();
}
}
}
Loading