diff --git a/docs/dev-tools/backend_architecture.md b/docs/dev-tools/backend_architecture.md index 1f2dae9721..70961ba4dd 100644 --- a/docs/dev-tools/backend_architecture.md +++ b/docs/dev-tools/backend_architecture.md @@ -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_` (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_` 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: diff --git a/docs/registry.md b/docs/registry.md index 1e39d65caa..b855ca0aa7 100644 --- a/docs/registry.md +++ b/docs/registry.md @@ -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_`. 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: ## Tools {#tools} diff --git a/e2e/backend/test_backend_env_override b/e2e/backend/test_backend_env_override new file mode 100755 index 0000000000..aeaea98256 --- /dev/null +++ b/e2e/backend/test_backend_env_override @@ -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" diff --git a/src/cli/args/backend_arg.rs b/src/cli/args/backend_arg.rs index e95682a0ad..6de894c088 100644 --- a/src/cli/args/backend_arg.rs +++ b/src/cli/args/backend_arg.rs @@ -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; @@ -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 diff --git a/src/registry.rs b/src/registry.rs index 0de0e8f0f2..e7a325ddba 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -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; @@ -32,8 +34,33 @@ pub struct RegistryBackend { pub platforms: &'static [&'static str], } +// Cache for environment variable overrides +static ENV_BACKENDS: Lazy>> = + 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]; + } + static BACKEND_TYPES: Lazy> = Lazy::new(|| { let mut backend_types = BackendType::iter() .map(|b| b.to_string()) @@ -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"); + } + ENV_BACKENDS.lock().unwrap().clear(); + } + } }