use crate::Result;
use crate::config::miserc;
use crate::env_diff::{EnvDiff, EnvMap};
use crate::file::replace_path;
use crate::shell::ShellType;
use crate::{cli::args::ToolArg, file::display_path};
use eyre::Context;
use indexmap::{IndexMap, IndexSet};
use itertools::Itertools;
use log::LevelFilter;
pub(crate) use std::env::*;
use std::process;
use std::sync::LazyLock as Lazy;
use std::sync::RwLock;
use std::{
collections::{HashMap, HashSet},
ffi::OsStr,
sync::Mutex,
};
use std::{path::Path, string::ToString};
use std::{path::PathBuf, sync::atomic::AtomicBool};
pub(crate) static ARGS: RwLock<Vec<String>> = RwLock::new(vec![]);
pub(crate) static TOOL_ARGS: RwLock<Vec<ToolArg>> = RwLock::new(vec![]);
pub(crate) const MISE_INSTALL_VERSION_ENV_VAR: &str = "MISE_INSTALL_VERSION";
pub(crate) const MISE_TOOL_VERSION_ENV_VAR: &str = "MISE_TOOL_VERSION";
pub(crate) const NON_TOOL_VERSION_ENV_VARS: &[&str] =
&[MISE_INSTALL_VERSION_ENV_VAR, MISE_TOOL_VERSION_ENV_VAR];
#[cfg(unix)]
pub(crate) static SHELL: Lazy<String> = Lazy::new(|| var("SHELL").unwrap_or_else(|_| "sh".into()));
#[cfg(windows)]
pub(crate) static SHELL: Lazy<String> =
Lazy::new(|| var("COMSPEC").unwrap_or_else(|_| "cmd.exe".into()));
pub(crate) static MISE_SHELL: Lazy<Option<ShellType>> =
Lazy::new(|| detect_shell(var("MISE_SHELL").ok(), var("SHELL").ok(), &SHELL));
fn detect_shell(
mise_shell: Option<String>,
shell_var: Option<String>,
fallback: &str,
) -> Option<ShellType> {
if let Some(s) = mise_shell {
return s.parse().ok();
}
if cfg!(windows)
&& let Some(st) = shell_var.and_then(|s| s.parse().ok())
{
return Some(st);
}
fallback.parse().ok()
}
#[cfg(unix)]
pub(crate) static SHELL_COMMAND_FLAG: &str = "-c";
#[cfg(windows)]
pub(crate) static SHELL_COMMAND_FLAG: &str = "/c";
#[cfg(test)]
pub(crate) static HOME: Lazy<PathBuf> =
Lazy::new(|| PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test"));
#[cfg(not(test))]
pub(crate) static HOME: Lazy<PathBuf> = Lazy::new(|| {
homedir::my_home()
.ok()
.flatten()
.unwrap_or_else(|| PathBuf::from("/"))
});
pub(crate) static EDITOR: Lazy<String> = Lazy::new(|| {
var("VISUAL")
.or_else(|_| var("EDITOR"))
.unwrap_or_else(|_| DEFAULT_EDITOR.to_string())
});
#[cfg(windows)]
const DEFAULT_EDITOR: &str = "notepad";
#[cfg(not(windows))]
const DEFAULT_EDITOR: &str = "nano";
#[cfg(macos)]
pub(crate) static XDG_CACHE_HOME: Lazy<PathBuf> =
Lazy::new(|| var_path("XDG_CACHE_HOME").unwrap_or_else(|| HOME.join("Library/Caches")));
#[cfg(windows)]
pub(crate) static XDG_CACHE_HOME: Lazy<PathBuf> = Lazy::new(|| {
var_path("XDG_CACHE_HOME")
.or_else(|| var_path("TEMP"))
.unwrap_or_else(temp_dir)
});
#[cfg(all(not(windows), not(macos)))]
pub(crate) static XDG_CACHE_HOME: Lazy<PathBuf> =
Lazy::new(|| var_path("XDG_CACHE_HOME").unwrap_or_else(|| HOME.join(".cache")));
pub(crate) static XDG_CONFIG_HOME: Lazy<PathBuf> =
Lazy::new(|| var_path("XDG_CONFIG_HOME").unwrap_or_else(|| HOME.join(".config")));
#[cfg(unix)]
pub(crate) static XDG_DATA_HOME: Lazy<PathBuf> =
Lazy::new(|| var_path("XDG_DATA_HOME").unwrap_or_else(|| HOME.join(".local").join("share")));
#[cfg(windows)]
pub(crate) static XDG_DATA_HOME: Lazy<PathBuf> = Lazy::new(|| {
var_path("XDG_DATA_HOME")
.or(var_path("LOCALAPPDATA"))
.unwrap_or_else(|| HOME.join("AppData").join("Local"))
});
pub(crate) static XDG_STATE_HOME: Lazy<PathBuf> =
Lazy::new(|| var_path("XDG_STATE_HOME").unwrap_or_else(|| HOME.join(".local").join("state")));
#[cfg(windows)]
pub(crate) static LOCAL_APPDATA: Lazy<PathBuf> =
Lazy::new(|| var_path("LOCALAPPDATA").unwrap_or_else(|| HOME.join("AppData").join("Local")));
pub(crate) static MISE_FRIENDLY_ERROR: Lazy<bool> = Lazy::new(|| {
if var_is_true("MISE_FRIENDLY_ERROR") {
true
} else if var_is_false("MISE_FRIENDLY_ERROR") {
false
} else {
!cfg!(debug_assertions) && log::max_level() < log::LevelFilter::Debug
}
});
pub(crate) static MISE_TOOL_STUB: Lazy<bool> =
Lazy::new(|| ARGS.read().unwrap().get(1).map(|s| s.as_str()) == Some("tool-stub"));
pub(crate) static MISE_NO_CONFIG: Lazy<bool> = Lazy::new(|| var_is_true("MISE_NO_CONFIG"));
pub(crate) static MISE_NO_ENV: Lazy<bool> = Lazy::new(|| var_is_true("MISE_NO_ENV"));
pub(crate) static MISE_NO_HOOKS: Lazy<bool> = Lazy::new(|| var_is_true("MISE_NO_HOOKS"));
pub(crate) static MISE_PROGRESS_TRACE: Lazy<bool> =
Lazy::new(|| var_is_true("MISE_PROGRESS_TRACE"));
pub(crate) static MISE_CACHE_DIR: Lazy<PathBuf> =
Lazy::new(|| var_path("MISE_CACHE_DIR").unwrap_or_else(|| XDG_CACHE_HOME.join("mise")));
pub(crate) static MISE_CONFIG_DIR: Lazy<PathBuf> =
Lazy::new(|| var_path("MISE_CONFIG_DIR").unwrap_or_else(|| XDG_CONFIG_HOME.join("mise")));
pub(crate) static MISE_DEFAULT_CONFIG_DIR: Lazy<PathBuf> =
Lazy::new(|| XDG_CONFIG_HOME.join("mise"));
pub(crate) static MISE_CONFIG_DIR_OVERRIDDEN: Lazy<bool> = Lazy::new(|| {
var_path("MISE_CONFIG_DIR").is_some() && *MISE_CONFIG_DIR != *MISE_DEFAULT_CONFIG_DIR
});
pub(crate) static MISE_DATA_DIR: Lazy<PathBuf> =
Lazy::new(|| var_path("MISE_DATA_DIR").unwrap_or_else(|| XDG_DATA_HOME.join("mise")));
pub(crate) static MISE_STATE_DIR: Lazy<PathBuf> =
Lazy::new(|| var_path("MISE_STATE_DIR").unwrap_or_else(|| XDG_STATE_HOME.join("mise")));
pub(crate) static MISE_TMP_DIR: Lazy<PathBuf> =
Lazy::new(|| var_path("MISE_TMP_DIR").unwrap_or_else(|| temp_dir().join("mise")));
pub(crate) static MISE_SYSTEM_CONFIG_DIR: Lazy<PathBuf> = Lazy::new(|| {
var_path("MISE_SYSTEM_CONFIG_DIR")
.or_else(|| var_path("MISE_SYSTEM_DIR"))
.unwrap_or_else(|| PathBuf::from("/etc/mise"))
});
pub(crate) static MISE_INSTALLS_DIR: Lazy<PathBuf> =
Lazy::new(|| var_path("MISE_INSTALLS_DIR").unwrap_or_else(|| MISE_DATA_DIR.join("installs")));
pub(crate) static MISE_DOWNLOADS_DIR: Lazy<PathBuf> =
Lazy::new(|| var_path("MISE_DOWNLOADS_DIR").unwrap_or_else(|| MISE_DATA_DIR.join("downloads")));
pub(crate) static MISE_PLUGINS_DIR: Lazy<PathBuf> =
Lazy::new(|| var_path("MISE_PLUGINS_DIR").unwrap_or_else(|| MISE_DATA_DIR.join("plugins")));
pub(crate) static MISE_SHIMS_DIR: Lazy<PathBuf> =
Lazy::new(|| var_path("MISE_SHIMS_DIR").unwrap_or_else(|| MISE_DATA_DIR.join("shims")));
pub(crate) static MISE_SYSTEM_DATA_DIR: Lazy<PathBuf> = Lazy::new(|| {
var_path("MISE_SYSTEM_DATA_DIR").unwrap_or_else(|| PathBuf::from("/usr/local/share/mise"))
});
pub(crate) static MISE_SYSTEM_INSTALLS_DIR: Lazy<PathBuf> =
Lazy::new(|| MISE_SYSTEM_DATA_DIR.join("installs"));
static MISE_SHARED_INSTALL_DIRS_ENV: Lazy<Vec<PathBuf>> = Lazy::new(|| {
var_os("MISE_SHARED_INSTALL_DIRS")
.map(|v| {
std::env::split_paths(&v)
.filter(|p| !p.as_os_str().is_empty())
.map(replace_path)
.collect()
})
.unwrap_or_default()
});
pub(crate) fn shared_install_dirs() -> Vec<PathBuf> {
use crate::config::Settings;
let user_dirs = if let std::result::Result::Ok(settings) = Settings::try_get()
&& let Some(ref dirs) = settings.shared_install_dirs
&& !dirs.is_empty()
{
dirs.clone()
} else {
MISE_SHARED_INSTALL_DIRS_ENV.clone()
};
let system = &*MISE_SYSTEM_INSTALLS_DIR;
let mut result = Vec::new();
if system.is_dir() && *system != *MISE_INSTALLS_DIR {
result.push(system.clone());
}
result.extend(user_dirs);
result
}
pub(crate) fn shared_install_dirs_early() -> Vec<PathBuf> {
let system = &*MISE_SYSTEM_INSTALLS_DIR;
let mut result = Vec::new();
if system.is_dir() && *system != *MISE_INSTALLS_DIR {
result.push(system.clone());
}
result.extend(MISE_SHARED_INSTALL_DIRS_ENV.iter().cloned());
result
}
pub(crate) fn install_path_category(path: &Path) -> InstallPathCategory {
if path.starts_with(&*MISE_SYSTEM_INSTALLS_DIR) {
InstallPathCategory::System
} else if shared_install_dirs().iter().any(|d| path.starts_with(d)) {
InstallPathCategory::Shared
} else {
InstallPathCategory::Local
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum InstallPathCategory {
Local,
System,
Shared,
}
pub(crate) fn find_in_shared_installs(
primary_path: PathBuf,
tool_dir_name: &str,
pathname: &str,
) -> PathBuf {
if !primary_path.exists() {
for shared_dir in shared_install_dirs() {
let shared_path = shared_dir.join(tool_dir_name).join(pathname);
if shared_path.exists() {
return shared_path;
}
}
}
primary_path
}
pub(crate) static MISE_DEFAULT_TOOL_VERSIONS_FILENAME: Lazy<String> = Lazy::new(|| {
var("MISE_DEFAULT_TOOL_VERSIONS_FILENAME")
.ok()
.or(MISE_OVERRIDE_TOOL_VERSIONS_FILENAMES
.as_ref()
.and_then(|v| v.first().cloned()))
.or(var("MISE_DEFAULT_TOOL_VERSIONS_FILENAME").ok())
.unwrap_or_else(|| ".tool-versions".into())
});
pub(crate) static MISE_DEFAULT_CONFIG_FILENAME: Lazy<String> = Lazy::new(|| {
var("MISE_DEFAULT_CONFIG_FILENAME")
.ok()
.or(MISE_OVERRIDE_CONFIG_FILENAMES.first().cloned())
.unwrap_or_else(|| "mise.toml".into())
});
pub(crate) static MISE_OVERRIDE_TOOL_VERSIONS_FILENAMES: Lazy<Option<IndexSet<String>>> =
Lazy::new(|| match var("MISE_OVERRIDE_TOOL_VERSIONS_FILENAMES") {
Ok(v) if v == "none" => Some([].into()),
Ok(v) => Some(split_colon_list(&v)),
Err(_) => {
miserc::get_override_tool_versions_filenames().map(|v| v.iter().cloned().collect())
}
});
pub(crate) static MISE_OVERRIDE_CONFIG_FILENAMES: Lazy<IndexSet<String>> =
Lazy::new(|| match var("MISE_OVERRIDE_CONFIG_FILENAMES") {
Ok(v) => split_colon_list(&v),
Err(_) => miserc::get_override_config_filenames()
.map(|v| v.iter().cloned().collect())
.unwrap_or_default(),
});
pub(crate) static MISE_ENV: Lazy<Vec<String>> = Lazy::new(|| environment(&ARGS.read().unwrap()));
pub(crate) fn auto_env_setting() -> Option<bool> {
if var_is_true("MISE_AUTO_ENV") {
Some(true)
} else if var_is_false("MISE_AUTO_ENV") {
Some(false)
} else {
miserc::get_auto_env()
}
}
pub(crate) fn env_conf_d_setting() -> Option<bool> {
if var_is_true("MISE_ENV_CONF_D") {
Some(true)
} else if var_is_false("MISE_ENV_CONF_D") {
Some(false)
} else {
miserc::get_env_conf_d()
}
}
pub(crate) fn env_conf_d_default_for_version(v: &versions::Versioning) -> bool {
*v >= versions::Versioning::new("2027.8.10").unwrap()
}
pub(crate) fn env_conf_d() -> bool {
env_conf_d_setting().unwrap_or_else(|| env_conf_d_default_for_version(&crate::cli::version::V))
}
pub(crate) fn auto_env_default_for_version(v: &versions::Versioning) -> bool {
*v >= versions::Versioning::new("2027.6.0").unwrap()
}
pub(crate) fn platform_env_names() -> Vec<String> {
let mut names: Vec<String> = vec![];
for name in [
consts::FAMILY.to_string(),
crate::cli::version::OS.to_string(),
format!(
"{}-{}",
*crate::cli::version::OS,
*crate::cli::version::ARCH
),
] {
if !names.contains(&name) {
names.push(name);
}
}
names
}
pub(crate) static AUTO_ENV_NAMES: Lazy<Vec<String>> = Lazy::new(|| {
let enabled =
auto_env_setting().unwrap_or_else(|| auto_env_default_for_version(&crate::cli::version::V));
if !enabled {
return vec![];
}
platform_env_names()
.into_iter()
.filter(|name| !MISE_ENV.contains(name))
.collect()
});
pub(crate) static MISE_ENV_WITH_AUTO: Lazy<Vec<String>> = Lazy::new(|| {
AUTO_ENV_NAMES
.iter()
.chain(MISE_ENV.iter())
.cloned()
.collect()
});
pub(crate) static MISE_GLOBAL_CONFIG_FILE: Lazy<Option<PathBuf>> =
Lazy::new(|| var_path("MISE_GLOBAL_CONFIG_FILE").or_else(|| var_path("MISE_CONFIG_FILE")));
pub(crate) static MISE_GLOBAL_CONFIG_ROOT: Lazy<PathBuf> =
Lazy::new(|| var_path("MISE_GLOBAL_CONFIG_ROOT").unwrap_or_else(|| HOME.to_path_buf()));
pub(crate) static MISE_SYSTEM_CONFIG_FILE: Lazy<Option<PathBuf>> =
Lazy::new(|| var_path("MISE_SYSTEM_CONFIG_FILE"));
pub(crate) static MISE_IGNORED_CONFIG_PATHS: Lazy<Vec<PathBuf>> = Lazy::new(|| {
let invocation_cwd = miserc::invocation_cwd()
.map(Path::to_path_buf)
.or_else(|| current_dir().ok())
.unwrap_or_default();
var_os("MISE_IGNORED_CONFIG_PATHS")
.map(|v| {
split_paths(&v)
.filter(|p| !p.as_os_str().is_empty())
.map(|p| miserc::resolve_ignored_config_path(p, &invocation_cwd))
.collect()
})
.or_else(|| miserc::get_ignored_config_paths().map(|paths| paths.iter().cloned().collect()))
.unwrap_or_default()
});
pub(crate) static MISE_CEILING_PATHS: Lazy<HashSet<PathBuf>> = Lazy::new(|| {
var_os("MISE_CEILING_PATHS")
.map(|v| {
split_paths(&v)
.filter(|p| !p.as_os_str().is_empty())
.map(replace_path)
.collect()
})
.or_else(|| {
miserc::get_ceiling_paths()
.map(|paths| paths.iter().cloned().map(replace_path).collect())
})
.unwrap_or_default()
});
pub(crate) static MISE_USE_TOML: Lazy<bool> = Lazy::new(|| !var_is_false("MISE_USE_TOML"));
pub(crate) static MISE_LIST_ALL_VERSIONS: Lazy<bool> =
Lazy::new(|| var_is_true("MISE_LIST_ALL_VERSIONS"));
pub(crate) static ARGV0: Lazy<String> = Lazy::new(|| ARGS.read().unwrap()[0].to_string());
pub(crate) static MISE_BIN_NAME: Lazy<&str> = Lazy::new(|| filename(&ARGV0));
pub(crate) static MISE_LOG_FILE: Lazy<Option<PathBuf>> = Lazy::new(|| var_path("MISE_LOG_FILE"));
pub(crate) static MISE_LOG_FILE_LEVEL: Lazy<Option<LevelFilter>> = Lazy::new(log_file_level);
fn find_in_tree(base: &Path, rels: &[&[&str]]) -> Option<PathBuf> {
for rel in rels {
let mut p = base.to_path_buf();
for part in *rel {
p = p.join(part);
}
if p.exists() {
return Some(p);
}
}
None
}
fn mise_install_base() -> Option<PathBuf> {
std::fs::canonicalize(&*MISE_BIN)
.ok()
.and_then(|p| p.parent().map(|p| p.to_path_buf()))
.and_then(|p| p.parent().map(|p| p.to_path_buf()))
}
pub(crate) static MISE_SELF_UPDATE_INSTRUCTIONS: Lazy<Option<PathBuf>> = Lazy::new(|| {
if let Some(p) = var_path("MISE_SELF_UPDATE_INSTRUCTIONS") {
return Some(p);
}
let base = mise_install_base()?;
find_in_tree(
&base,
&[
&["lib", "mise-self-update-instructions.toml"],
&["lib", "mise", "mise-self-update-instructions.toml"],
&["lib64", "mise", "mise-self-update-instructions.toml"],
],
)
});
#[cfg(feature = "self_update")]
pub(crate) static MISE_SELF_UPDATE_AVAILABLE: Lazy<Option<bool>> = Lazy::new(|| {
if var_is_true("MISE_SELF_UPDATE_AVAILABLE") {
Some(true)
} else if var_is_false("MISE_SELF_UPDATE_AVAILABLE") {
Some(false)
} else {
None
}
});
#[cfg(feature = "self_update")]
pub(crate) static MISE_SELF_UPDATE_DISABLED_PATH: Lazy<Option<PathBuf>> = Lazy::new(|| {
let base = mise_install_base()?;
find_in_tree(
&base,
&[
&["lib", ".disable-self-update"],
&["lib", "mise", ".disable-self-update"],
&["lib64", "mise", ".disable-self-update"],
],
)
});
pub(crate) static MISE_LOG_HTTP: Lazy<bool> = Lazy::new(|| var_is_true("MISE_LOG_HTTP"));
pub(crate) static MISE_LOG_VERBOSE_DEPS: Lazy<bool> =
Lazy::new(|| var_is_true("MISE_LOG_VERBOSE_DEPS"));
pub(crate) static __USAGE: Lazy<Option<String>> = Lazy::new(|| var("__USAGE").ok());
pub(crate) static __MISE_SHIM: Lazy<bool> = Lazy::new(|| var_is_true("__MISE_SHIM"));
pub(crate) const MISE_SHIM_PATH_ENV: &str = "__MISE_SHIM_PATH";
pub(crate) static MISE_SHIM_PATH: Lazy<RwLock<Option<PathBuf>>> =
Lazy::new(|| RwLock::new(var_path(MISE_SHIM_PATH_ENV)));
pub(crate) static IS_RUNNING_AS_SHIM: Lazy<bool> = Lazy::new(|| {
if cfg!(test) {
return false;
}
if *MISE_TOOL_STUB {
return true;
}
let bin_name = *MISE_BIN_NAME;
!is_mise_binary(bin_name)
});
pub(crate) fn is_mise_binary(bin_name: &str) -> bool {
let is_mise = |s: &str| {
if cfg!(windows) {
s.eq_ignore_ascii_case("mise")
} else {
s == "mise"
}
};
is_mise(bin_name)
|| bin_name
.split_once(['.', '-'])
.is_some_and(|(stem, _)| is_mise(stem))
}
#[cfg_attr(not(windows), allow(dead_code))]
pub(crate) const SELF_REPLACE_SUFFIXES: [&str; 2] = [".__selfdelete__.exe", ".__relocated__.exe"];
#[cfg_attr(not(windows), allow(dead_code))]
pub(crate) fn is_self_replace_helper(bin_name: &str, exe_stem: &str) -> bool {
let prefix = format!(".{exe_stem}.");
let matches = bin_name.get(..prefix.len()).is_some_and(|head| {
if cfg!(windows) {
head.eq_ignore_ascii_case(&prefix)
} else {
head == prefix
}
});
if !matches {
return false;
}
let rest = &bin_name[prefix.len()..];
SELF_REPLACE_SUFFIXES.iter().any(|suffix| {
rest.strip_suffix(suffix)
.is_some_and(is_self_replace_random_segment)
})
}
#[cfg_attr(not(windows), allow(dead_code))]
pub(crate) const SELF_REPLACE_RANDOM_LEN: usize = 32;
#[cfg_attr(not(windows), allow(dead_code))]
fn is_self_replace_random_segment(s: &str) -> bool {
s.len() == SELF_REPLACE_RANDOM_LEN && s.bytes().all(|b| b.is_ascii_lowercase())
}
#[cfg(windows)]
pub(crate) fn invoked_as_self_replace_helper() -> bool {
let Some(invoked) = std::env::args_os().next() else {
return false;
};
let Some(name) = Path::new(&invoked).file_name().and_then(|n| n.to_str()) else {
return false;
};
name.starts_with('.')
&& SELF_REPLACE_SUFFIXES.iter().any(|suffix| {
name.strip_suffix(suffix).is_some_and(|head| {
head.rsplit_once('.').is_some_and(|(stem, random)| {
!stem.is_empty() && is_self_replace_random_segment(random)
})
})
})
}
#[cfg(test)]
pub(crate) static TERM_WIDTH_OVERRIDE: Lazy<Option<usize>> = Lazy::new(|| None);
#[cfg(not(test))]
pub(crate) static TERM_WIDTH_OVERRIDE: Lazy<Option<usize>> = Lazy::new(|| {
for key in ["MISE_TERM_WIDTH", "COLUMNS"] {
if let Some(w) = var(key)
.ok()
.and_then(|v| v.trim().parse::<usize>().ok())
.filter(|w| *w > 0)
{
if key == "COLUMNS" {
debug!(
"overriding terminal width with COLUMNS={w}; set MISE_TERM_WIDTH to control this explicitly"
);
}
return Some(w);
}
}
None
});
#[cfg(test)]
pub(crate) static TERM_WIDTH: Lazy<usize> = Lazy::new(|| 80);
#[cfg(not(test))]
pub(crate) static TERM_WIDTH: Lazy<usize> = Lazy::new(|| {
if let Some(w) = *TERM_WIDTH_OVERRIDE {
return w;
}
terminal_size::terminal_size()
.map(|(w, _)| w.0 as usize)
.unwrap_or(80)
.max(80)
});
pub(crate) static MISE_BIN: Lazy<PathBuf> = Lazy::new(|| {
var_path("__MISE_BIN")
.or_else(|| current_exe().ok())
.unwrap_or_else(|| "mise".into())
});
pub(crate) static MISE_TIMINGS: Lazy<u8> = Lazy::new(|| var_u8("MISE_TIMINGS"));
pub(crate) static MISE_PID: Lazy<String> = Lazy::new(|| process::id().to_string());
pub(crate) static MISE_JOBS: Lazy<Option<usize>> =
Lazy::new(|| var("MISE_JOBS").ok().and_then(|v| v.parse::<usize>().ok()));
pub(crate) static __MISE_SCRIPT: Lazy<bool> = Lazy::new(|| var_is_true("__MISE_SCRIPT"));
pub(crate) static __MISE_DIFF: Lazy<EnvDiff> = Lazy::new(get_env_diff);
pub(crate) static __MISE_ORIG_PATH: Lazy<Option<String>> =
Lazy::new(|| var("__MISE_ORIG_PATH").ok());
pub(crate) static __MISE_ZSH_PRECMD_RUN: Lazy<bool> =
Lazy::new(|| !var_is_false("__MISE_ZSH_PRECMD_RUN"));
pub(crate) static LINUX_DISTRO: Lazy<Option<String>> = Lazy::new(linux_distro);
pub(crate) static PREFER_OFFLINE: Lazy<AtomicBool> =
Lazy::new(|| prefer_offline(&ARGS.read().unwrap()).into());
pub(crate) static REMOTE_FETCH_COMMAND: Lazy<AtomicBool> =
Lazy::new(|| remote_fetch_command(&ARGS.read().unwrap()).into());
pub(crate) static OFFLINE: Lazy<bool> = Lazy::new(|| offline(&ARGS.read().unwrap()));
pub(crate) static WARN_ON_MISSING_REQUIRED_ENV: Lazy<bool> =
Lazy::new(|| warn_on_missing_required_env(&ARGS.read().unwrap()));
pub(crate) static PRISTINE_ENV: Lazy<EnvMap> =
Lazy::new(|| get_pristine_env(&__MISE_DIFF, vars_safe().collect()));
pub(crate) static PATH_KEY: Lazy<String> =
Lazy::new(|| path_key_from_env(vars_os().filter_map(|(k, _)| k.into_string().ok())));
#[cfg(unix)]
fn path_key_from_env(_keys: impl IntoIterator<Item = String>) -> String {
"PATH".into()
}
#[cfg(windows)]
fn path_key_from_env(keys: impl IntoIterator<Item = String>) -> String {
keys.into_iter()
.find(|k| k.eq_ignore_ascii_case("PATH"))
.unwrap_or("PATH".into())
}
pub(crate) fn is_path_key(key: &str) -> bool {
if cfg!(windows) {
key.eq_ignore_ascii_case(&PATH_KEY)
} else {
key == *PATH_KEY
}
}
pub(crate) fn normalize_path_key(key: String) -> String {
if is_path_key(&key) {
PATH_KEY.to_string()
} else {
key
}
}
pub(crate) static PATH: Lazy<Vec<PathBuf>> = Lazy::new(|| match PRISTINE_ENV.get(&*PATH_KEY) {
Some(path) => split_paths(path).collect(),
None => vec![],
});
pub(crate) static PATH_NON_PRISTINE: Lazy<Vec<PathBuf>> = Lazy::new(|| match var(&*PATH_KEY) {
Ok(ref path) => split_paths(path).collect(),
Err(_) => vec![],
});
pub(crate) static DIRENV_DIFF: Lazy<Option<String>> = Lazy::new(|| var("DIRENV_DIFF").ok());
pub(crate) static GITHUB_TOKEN: Lazy<Option<String>> =
Lazy::new(|| get_token(&["MISE_GITHUB_TOKEN", "GITHUB_API_TOKEN", "GITHUB_TOKEN"]));
pub(crate) static MISE_GITHUB_ENTERPRISE_TOKEN: Lazy<Option<String>> =
Lazy::new(|| get_token(&["MISE_GITHUB_ENTERPRISE_TOKEN"]));
pub(crate) static GITLAB_TOKEN: Lazy<Option<String>> =
Lazy::new(|| get_token(&["MISE_GITLAB_TOKEN", "GITLAB_TOKEN"]));
pub(crate) static MISE_GITLAB_ENTERPRISE_TOKEN: Lazy<Option<String>> =
Lazy::new(|| get_token(&["MISE_GITLAB_ENTERPRISE_TOKEN"]));
pub(crate) static MISE_FORGEJO_ENTERPRISE_TOKEN: Lazy<Option<String>> =
Lazy::new(|| get_token(&["MISE_FORGEJO_ENTERPRISE_TOKEN"]));
pub(crate) static TEST_TRANCHE: Lazy<usize> = Lazy::new(|| var_u8("TEST_TRANCHE") as usize);
pub(crate) static TEST_TRANCHE_COUNT: Lazy<usize> =
Lazy::new(|| var_u8("TEST_TRANCHE_COUNT") as usize);
pub(crate) static CLICOLOR_FORCE: Lazy<Option<bool>> =
Lazy::new(|| var("CLICOLOR_FORCE").ok().map(|v| v != "0"));
pub(crate) static CLICOLOR: Lazy<Option<bool>> = Lazy::new(|| {
if *CLICOLOR_FORCE == Some(true) {
Some(true)
} else if *NO_COLOR || var_is_false("MISE_COLOR") {
Some(false)
} else if let Ok(v) = var("CLICOLOR") {
Some(v != "0")
} else {
None
}
});
pub(crate) static NO_COLOR: Lazy<bool> = Lazy::new(|| var("NO_COLOR").is_ok_and(|v| !v.is_empty()));
pub(crate) static MISE_FORCE_PROGRESS: Lazy<bool> =
Lazy::new(|| var_is_true("MISE_FORCE_PROGRESS"));
pub(crate) static PYENV_ROOT: Lazy<PathBuf> =
Lazy::new(|| var_path("PYENV_ROOT").unwrap_or_else(|| HOME.join(".pyenv")));
pub(crate) static UV_PYTHON_INSTALL_DIR: Lazy<PathBuf> = Lazy::new(|| {
var_path("UV_PYTHON_INSTALL_DIR").unwrap_or_else(|| XDG_DATA_HOME.join("uv").join("python"))
});
fn get_env_diff() -> EnvDiff {
let env = vars_safe().collect::<HashMap<_, _>>();
match env.get("__MISE_DIFF") {
Some(raw) => EnvDiff::deserialize(raw).unwrap_or_else(|err| {
warn!("Failed to deserialize __MISE_DIFF: {:#}", err);
EnvDiff::default()
}),
None => EnvDiff::default(),
}
}
fn var_u8(key: &str) -> u8 {
var(key)
.ok()
.and_then(|v| v.parse::<u8>().ok())
.unwrap_or_default()
}
pub(crate) fn var_is_true(key: &str) -> bool {
match var(key) {
Ok(v) => {
let v = v.to_lowercase();
v == "y" || v == "yes" || v == "true" || v == "1" || v == "on"
}
Err(_) => false,
}
}
fn var_is_false(key: &str) -> bool {
match var(key) {
Ok(v) => {
let v = v.to_lowercase();
v == "n" || v == "no" || v == "false" || v == "0" || v == "off"
}
Err(_) => false,
}
}
pub(crate) fn in_home_dir() -> bool {
current_dir().is_ok_and(|d| d == *HOME)
}
pub(crate) fn var_path(key: &str) -> Option<PathBuf> {
var_os(key)
.map(PathBuf::from)
.map(replace_path)
.filter(|p| !p.as_os_str().is_empty())
}
fn get_pristine_env(mise_diff: &EnvDiff, orig_env: EnvMap) -> EnvMap {
let mut env = reverse_diff_preserving_overrides(mise_diff, orig_env);
let path = match env.get(&*PATH_KEY) {
Some(path) => split_paths(path).collect(),
None => vec![],
};
let mut to_remove = mise_diff.path.iter().collect::<HashSet<_>>();
let path = path
.into_iter()
.filter(|p| !to_remove.remove(p))
.collect_vec();
env.insert(
PATH_KEY.to_string(),
join_paths(path).unwrap().to_string_lossy().to_string(),
);
env
}
fn reverse_diff_preserving_overrides(mise_diff: &EnvDiff, mut env: EnvMap) -> EnvMap {
for (key, old_value) in &mise_diff.old {
match env_diff_get(&mise_diff.new, key) {
Some(new_value) if env_map_get(&env, key) == Some(new_value) => {
let key = env_map_key(&env, key)
.cloned()
.unwrap_or_else(|| key.clone());
env.insert(key, old_value.clone());
}
None if env_map_get(&env, key).is_none() => {
env.insert(key.clone(), old_value.clone());
}
_ => {}
}
}
for (key, new_value) in &mise_diff.new {
if env_diff_get(&mise_diff.old, key).is_none()
&& env_map_get(&env, key) == Some(new_value)
&& let Some(key) = env_map_key(&env, key).cloned()
{
env.remove(&key);
}
}
env
}
#[cfg(not(windows))]
fn env_map_key<'a>(env: &'a EnvMap, key: &str) -> Option<&'a String> {
env.get_key_value(key).map(|(key, _)| key)
}
#[cfg(windows)]
fn env_map_key<'a>(env: &'a EnvMap, key: &str) -> Option<&'a String> {
env.keys()
.find(|candidate| windows_env_key_eq(candidate, key))
}
fn env_map_get<'a>(env: &'a EnvMap, key: &str) -> Option<&'a String> {
env_map_key(env, key).and_then(|key| env.get(key))
}
#[cfg(not(windows))]
fn env_diff_get<'a>(env: &'a IndexMap<String, String>, key: &str) -> Option<&'a String> {
env.get(key)
}
#[cfg(windows)]
fn env_diff_get<'a>(env: &'a IndexMap<String, String>, key: &str) -> Option<&'a String> {
env.iter()
.find(|(candidate, _)| windows_env_key_eq(candidate, key))
.map(|(_, value)| value)
}
#[cfg(windows)]
fn windows_env_key_eq(left: &str, right: &str) -> bool {
use windows_sys::Win32::Globalization::{CSTR_EQUAL, CompareStringOrdinal};
let left = left.encode_utf16().collect::<Vec<_>>();
let right = right.encode_utf16().collect::<Vec<_>>();
let (Ok(left_len), Ok(right_len)) = (i32::try_from(left.len()), i32::try_from(right.len()))
else {
return false;
};
unsafe {
CompareStringOrdinal(left.as_ptr(), left_len, right.as_ptr(), right_len, 1) == CSTR_EQUAL
}
}
fn offline(args: &[String]) -> bool {
if var_is_true("MISE_OFFLINE") {
return true;
}
args.iter()
.take_while(|a| *a != "--")
.any(|a| a == "--offline")
}
fn prefer_offline(args: &[String]) -> bool {
if var_is_true("MISE_PREFER_OFFLINE") {
return true;
}
let settings_args_end = first_non_global_arg_idx(args).unwrap_or(args.len());
if args[..settings_args_end]
.iter()
.any(|arg| arg == "--prefer-offline")
{
return true;
}
prefer_offline_command(args)
}
const PREFER_OFFLINE_COMMANDS: &[&str] = &[
"activate", "current", "direnv", "env", "exec", "hook-env", "ls", "where", "which", "x",
];
const REMOTE_FETCH_COMMANDS: &[&str] = &[
"lock",
"ls-remote",
"list-all",
"list-remote",
"outdated",
"upgrade",
"up",
];
fn first_non_global_arg_idx(args: &[String]) -> Option<usize> {
crate::cli::first_non_global_arg_idx_cached(args)
}
fn is_command(args: &[String], command_idx: Option<usize>, names: &[&str]) -> bool {
command_idx
.and_then(|idx| args.get(idx))
.map(|a| names.contains(&a.as_str()))
.unwrap_or_default()
}
fn prefer_offline_command(args: &[String]) -> bool {
is_command(
args,
first_non_global_arg_idx(args),
PREFER_OFFLINE_COMMANDS,
)
}
fn remote_fetch_command(args: &[String]) -> bool {
is_command(args, first_non_global_arg_idx(args), REMOTE_FETCH_COMMANDS)
}
fn warn_on_missing_required_env(args: &[String]) -> bool {
args.iter()
.take_while(|a| *a != "--")
.filter(|a| !a.starts_with('-'))
.nth(1)
.map(|a| {
[
"hook-env", ]
.contains(&a.as_str())
})
.unwrap_or_default()
}
fn environment(args: &[String]) -> Vec<String> {
let arg_defs = HashSet::from(["--profile", "-P", "--env", "-E"]);
let from_args = if *IS_RUNNING_AS_SHIM {
vec![]
} else {
let run_subcommands: HashSet<&str> = HashSet::from(["run", "r"]);
let mut values = Vec::new();
let mut it = args.iter().take_while(|a| a.as_str() != "--");
let mut in_run_subcommand = false;
while let Some(arg) = it.next() {
if arg.starts_with('-') {
if arg_defs.contains(arg.as_str()) {
if let Some(next) = it.next() {
values.push(next.to_string());
}
} else if let Some((prefix, rest)) = arg.split_at_checked(2)
&& !rest.starts_with('=')
&& arg_defs.contains(prefix)
{
values.push(rest.to_string());
} else if let Some((flag, value)) = arg.split_once('=') {
if arg_defs.contains(flag) {
values.push(value.to_string());
}
}
} else {
if in_run_subcommand {
break;
}
if run_subcommands.contains(arg.as_str()) {
in_run_subcommand = true;
}
}
}
values
.into_iter()
.flat_map(|s| {
s.split(',')
.filter(|s| !s.is_empty())
.map(String::from)
.collect::<Vec<_>>()
})
.collect()
};
if !from_args.is_empty() {
return from_args;
}
var("MISE_ENV")
.ok()
.or_else(|| var("MISE_PROFILE").ok())
.or_else(|| var("MISE_ENVIRONMENT").ok())
.map(|s| {
s.split(',')
.filter(|s| !s.is_empty())
.map(String::from)
.collect()
})
.or_else(|| miserc::get_env().cloned())
.unwrap_or_default()
}
fn log_file_level() -> Option<LevelFilter> {
let log_level = var("MISE_LOG_FILE_LEVEL").unwrap_or_default();
log_level.parse::<LevelFilter>().ok()
}
fn linux_distro() -> Option<String> {
crate::platform::linux_os_release().map(|release| release.id.clone())
}
fn split_colon_list(value: &str) -> IndexSet<String> {
value
.split(':')
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
.collect()
}
fn filename(path: &str) -> &str {
Path::new(path)
.file_name()
.and_then(|name| name.to_str())
.unwrap_or(path)
}
fn get_token(keys: &[&str]) -> Option<String> {
keys.iter()
.find_map(|key| var(key).ok())
.filter(|v| !v.trim().is_empty())
}
pub(crate) fn is_activated() -> bool {
var("__MISE_DIFF").is_ok()
}
pub(crate) fn set_var<K: AsRef<OsStr>, V: AsRef<OsStr>>(key: K, value: V) {
static MUTEX: Mutex<()> = Mutex::new(());
let _mutex = MUTEX.lock().unwrap();
unsafe {
std::env::set_var(key, value);
}
}
pub(crate) fn remove_var<K: AsRef<OsStr>>(key: K) {
static MUTEX: Mutex<()> = Mutex::new(());
let _mutex = MUTEX.lock().unwrap();
unsafe {
std::env::remove_var(key);
}
}
pub(crate) fn reset_env_cache_key() {
remove_var("__MISE_ENV_CACHE_KEY");
}
pub(crate) fn vars_safe() -> impl Iterator<Item = (String, String)> {
vars_os().filter_map(|(k, v)| {
let k_str = k.to_str()?;
let v_str = v.to_str()?;
Some((k_str.to_string(), v_str.to_string()))
})
}
pub(crate) fn args_safe() -> Vec<String> {
args_os().map(|a| a.to_string_lossy().to_string()).collect()
}
pub(crate) fn set_current_dir<P: AsRef<Path>>(path: P) -> Result<()> {
let path = path.as_ref();
trace!("cd {}", display_path(path));
std::env::set_current_dir(path)
.wrap_err_with(|| format!("failed to set current directory to {}", display_path(path)))?;
Ok(())
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use crate::config::Config;
use super::*;
#[test]
fn test_reverse_diff_preserves_runtime_overrides() {
let diff = EnvDiff {
old: [
("CHANGED".into(), "before".into()),
("REMOVED".into(), "before".into()),
]
.into(),
new: [
("ADDED".into(), "managed".into()),
("CHANGED".into(), "managed".into()),
]
.into(),
..Default::default()
};
let current = [
("ADDED".into(), "override".into()),
("CHANGED".into(), "override".into()),
("REMOVED".into(), "override".into()),
]
.into();
assert_eq!(
reverse_diff_preserving_overrides(&diff, current),
[
("ADDED".into(), "override".into()),
("CHANGED".into(), "override".into()),
("REMOVED".into(), "override".into()),
]
.into()
);
}
#[test]
fn test_reverse_diff_restores_unchanged_managed_values() {
let diff = EnvDiff {
old: [
("CHANGED".into(), "before".into()),
("REMOVED".into(), "before".into()),
]
.into(),
new: [
("ADDED".into(), "managed".into()),
("CHANGED".into(), "managed".into()),
]
.into(),
..Default::default()
};
let current = [
("ADDED".into(), "managed".into()),
("CHANGED".into(), "managed".into()),
]
.into();
assert_eq!(
reverse_diff_preserving_overrides(&diff, current),
[
("CHANGED".into(), "before".into()),
("REMOVED".into(), "before".into()),
]
.into()
);
}
#[test]
fn test_reverse_diff_preserves_runtime_removals() {
let diff = EnvDiff {
old: [("CHANGED".into(), "before".into())].into(),
new: [
("ADDED".into(), "managed".into()),
("CHANGED".into(), "managed".into()),
]
.into(),
..Default::default()
};
assert_eq!(
reverse_diff_preserving_overrides(&diff, EnvMap::new()),
EnvMap::new()
);
}
#[cfg(windows)]
#[test]
fn test_reverse_diff_matches_environment_keys_case_insensitively_on_windows() {
let diff = EnvDiff {
old: [
("Changed".into(), "before".into()),
("MÎSE_FOO".into(), "before-unicode".into()),
]
.into(),
new: [
("Added".into(), "managed".into()),
("Changed".into(), "managed".into()),
("MÎSE_FOO".into(), "managed-unicode".into()),
]
.into(),
..Default::default()
};
let current = [
("ADDED".into(), "managed".into()),
("CHANGED".into(), "managed".into()),
("mîse_foo".into(), "managed-unicode".into()),
]
.into();
assert_eq!(
reverse_diff_preserving_overrides(&diff, current),
[
("CHANGED".into(), "before".into()),
("mîse_foo".into(), "before-unicode".into()),
]
.into()
);
}
#[tokio::test]
async fn test_var_path() {
let _config = Config::get().await.unwrap();
set_var("MISE_TEST_PATH", "/foo/bar");
assert_eq!(
var_path("MISE_TEST_PATH").unwrap(),
PathBuf::from("/foo/bar")
);
remove_var("MISE_TEST_PATH");
}
#[tokio::test]
async fn test_var_path_treats_empty_as_unset() {
let _config = Config::get().await.unwrap();
set_var("MISE_TEST_EMPTY_PATH", "");
assert_eq!(var_path("MISE_TEST_EMPTY_PATH"), None);
remove_var("MISE_TEST_EMPTY_PATH");
}
#[cfg(unix)]
#[test]
fn test_vars_safe_skips_invalid_utf8() {
use std::ffi::OsString;
use std::os::unix::ffi::OsStringExt;
let bad_value_key = "MISE_TEST_VARS_SAFE_BAD_VALUE";
let bad_key = OsString::from_vec(b"MISE_TEST_VARS_SAFE_BAD_KEY_\xff".to_vec());
let good_key = "MISE_TEST_VARS_SAFE_GOOD";
let mut guard = crate::test::EnvVarGuard::new();
guard
.set(bad_value_key, OsString::from_vec(vec![0xff]))
.set(&bad_key, "ok")
.set(good_key, "1");
assert!(vars_os().any(|(k, _)| k == bad_value_key));
assert!(vars_os().any(|(k, _)| k == bad_key));
let safe: Vec<(String, String)> = vars_safe().collect();
assert!(!safe.iter().any(|(k, _)| k == bad_value_key));
assert!(
!safe
.iter()
.any(|(k, _)| k.starts_with("MISE_TEST_VARS_SAFE_BAD_KEY_"))
);
assert!(safe.iter().any(|(k, v)| k == good_key && v == "1"));
}
#[test]
fn test_auto_env_default_for_version() {
let v = |s: &str| versions::Versioning::new(s).unwrap();
assert!(!auto_env_default_for_version(&v("2026.6.2")));
assert!(!auto_env_default_for_version(&v("2026.12.0")));
assert!(!auto_env_default_for_version(&v("2027.5.9")));
assert!(auto_env_default_for_version(&v("2027.6.0")));
assert!(auto_env_default_for_version(&v("2028.1.0")));
}
#[test]
fn test_env_conf_d_default_for_version() {
let v = |s: &str| versions::Versioning::new(s).unwrap();
assert!(!env_conf_d_default_for_version(&v("2026.8.10")));
assert!(!env_conf_d_default_for_version(&v("2027.8.9")));
assert!(env_conf_d_default_for_version(&v("2027.8.10")));
}
#[test]
fn test_remote_fetch_command_skips_global_option_values() {
let args = |args: &[&str]| {
args.iter()
.map(|arg| (*arg).to_string())
.collect::<Vec<_>>()
};
assert!(remote_fetch_command(&args(&[
"mise", "--cd", "/tmp", "lock"
])));
assert!(remote_fetch_command(&args(&[
"mise",
"--profile",
"development",
"ls-remote",
])));
assert!(remote_fetch_command(&args(&[
"mise",
"--cd=/tmp",
"--profile=development",
"outdated",
])));
}
#[test]
fn test_prefer_offline_command_skips_global_option_values() {
let args = |args: &[&str]| {
args.iter()
.map(|arg| (*arg).to_string())
.collect::<Vec<_>>()
};
assert!(prefer_offline_command(&args(&[
"mise", "--cd", "/tmp", "activate"
])));
assert!(prefer_offline_command(&args(&[
"mise",
"--profile",
"development",
"hook-env",
])));
assert!(prefer_offline_command(&args(&["mise", "-C/tmp", "env",])));
assert!(!prefer_offline_command(&args(&[
"mise", "--cd", "/tmp", "lock"
])));
assert!(prefer_offline(&args(&[
"mise",
"--cd",
"/tmp",
"--prefer-offline",
"lock",
])));
}
#[cfg(unix)]
#[test]
fn test_platform_env_names_unix() {
let names = platform_env_names();
assert_eq!(names.len(), 3);
assert_eq!(names[0], "unix");
assert_eq!(names[1], *crate::cli::version::OS);
assert_eq!(
names[2],
format!(
"{}-{}",
*crate::cli::version::OS,
*crate::cli::version::ARCH
)
);
}
#[cfg(windows)]
#[test]
fn test_platform_env_names_windows() {
let names = platform_env_names();
assert_eq!(
names,
vec![
"windows".to_string(),
format!("windows-{}", *crate::cli::version::ARCH)
]
);
}
#[cfg(unix)]
#[test]
fn test_path_key_from_env_uses_uppercase_path_on_unix() {
assert_eq!(
path_key_from_env(vec!["path".into(), "HOME".into()]),
"PATH"
);
assert_eq!(
path_key_from_env(vec!["Path".into(), "HOME".into()]),
"PATH"
);
}
#[cfg(windows)]
#[test]
fn test_path_key_from_env_preserves_windows_path_casing() {
assert_eq!(
path_key_from_env(vec!["Path".into(), "TEMP".into()]),
"Path"
);
assert_eq!(
path_key_from_env(vec!["TEMP".into(), "PATH".into()]),
"PATH"
);
assert_eq!(path_key_from_env(vec!["TEMP".into()]), "PATH");
}
#[cfg(windows)]
#[test]
fn test_is_path_key_accepts_any_casing_on_windows() {
for spelling in ["PATH", "Path", "path", "pAtH"] {
assert!(is_path_key(spelling), "{spelling}");
assert_eq!(normalize_path_key(spelling.to_string()), *PATH_KEY);
}
assert!(!is_path_key("PATHEXT"));
assert!(!is_path_key("TEMP"));
assert!(!is_path_key(""));
assert_eq!(normalize_path_key("Temp".to_string()), "Temp");
}
#[cfg(unix)]
#[test]
fn test_is_path_key_is_exact_on_unix() {
assert!(is_path_key("PATH"));
assert_eq!(normalize_path_key("PATH".to_string()), "PATH");
assert!(!is_path_key("Path"));
assert!(!is_path_key("path"));
assert_eq!(normalize_path_key("Path".to_string()), "Path");
assert!(!is_path_key("PATHEXT"));
assert!(!is_path_key(""));
}
#[test]
fn test_split_colon_list() {
let cases: Vec<(&str, Vec<&str>)> = vec![
("", vec![]), (":", vec![]), (":::", vec![]), ("mise.toml", vec!["mise.toml"]),
("a:b", vec!["a", "b"]),
(":a:b:", vec!["a", "b"]), ("a::b", vec!["a", "b"]), ];
for (input, expected) in cases {
let result = split_colon_list(input);
let expected: IndexSet<String> = expected.into_iter().map(|s| s.to_string()).collect();
assert_eq!(result, expected, "input: {input:?}");
}
}
#[test]
fn test_token_overwrite() {
remove_var("MISE_GITHUB_TOKEN");
remove_var("GITHUB_TOKEN");
remove_var("GITHUB_API_TOKEN");
set_var("MISE_GITHUB_TOKEN", "");
set_var("GITHUB_TOKEN", "invalid_token");
assert_eq!(
get_token(&["MISE_GITHUB_TOKEN", "GITHUB_TOKEN"]),
None,
"Empty token should overwrite other tokens"
);
assert_eq!(
get_token(&["GITHUB_API_TOKEN", "GITHUB_TOKEN"]),
Some("invalid_token".into()),
"Unset token should not overwrite other tokens"
);
remove_var("MISE_GITHUB_TOKEN");
remove_var("GITHUB_TOKEN");
remove_var("GITHUB_API_TOKEN");
}
#[test]
fn test_filename_takes_the_basename() {
assert_eq!(filename("C:/Users/alice/.cargo/bin/mise.EXE"), "mise.EXE");
assert_eq!(filename("/usr/local/bin/mise"), "mise");
assert_eq!(filename("/usr/local/bin/mise/"), "mise");
assert_eq!(filename("mise"), "mise");
assert_eq!(filename("mise.exe"), "mise.exe");
}
#[cfg(windows)]
#[test]
fn test_filename_splits_on_backslash_on_windows() {
assert_eq!(filename(r"C:\Users\alice\.cargo\bin\mise.EXE"), "mise.EXE");
assert_eq!(filename(r"C:\tools\mise\"), "mise");
}
#[cfg(unix)]
#[test]
fn test_filename_keeps_backslashes_on_unix() {
assert_eq!(
filename(r"C:\Users\alice\.cargo\bin\mise.EXE"),
r"C:\Users\alice\.cargo\bin\mise.EXE"
);
assert_eq!(filename(r"/opt/odd/weird\name"), r"weird\name");
}
#[test]
fn test_a_full_path_argv0_is_recognised_as_mise_itself() {
for argv0 in [
"C:/Users/alice/.cargo/bin/mise.EXE",
"/usr/local/bin/mise",
"mise",
] {
assert!(
is_mise_binary(filename(argv0)),
"argv[0] {argv0:?} should be recognised as mise, not a shim"
);
}
#[cfg(windows)]
assert!(
is_mise_binary(filename(r"C:\Users\alice\.cargo\bin\mise.exe")),
"a backslash argv[0] should be recognised as mise on Windows"
);
for argv0 in ["/home/alice/.local/share/mise/shims/node", "node.exe"] {
assert!(
!is_mise_binary(filename(argv0)),
"argv[0] {argv0:?} should still be a shim"
);
}
}
#[test]
fn test_is_mise_binary() {
assert!(is_mise_binary("mise"));
assert!(is_mise_binary("mise.exe"));
assert!(is_mise_binary("mise.cmd"));
assert!(is_mise_binary("mise-doctor"));
assert!(!is_mise_binary("node"));
assert!(!is_mise_binary("node.exe"));
assert!(!is_mise_binary("misex"));
assert!(!is_mise_binary("misex.exe"));
}
#[cfg(windows)]
#[test]
fn test_is_mise_binary_ignores_case_on_windows() {
assert!(is_mise_binary("MISE.EXE"));
assert!(is_mise_binary("Mise.exe"));
assert!(is_mise_binary("MISE"));
assert!(is_mise_binary("MISE-DOCTOR"));
assert!(!is_mise_binary("NODE.EXE"));
assert!(!is_mise_binary("MISEX.EXE"));
}
#[cfg(unix)]
#[test]
fn test_is_mise_binary_is_case_sensitive_on_unix() {
assert!(!is_mise_binary("MISE"));
assert!(!is_mise_binary("Mise"));
assert!(!is_mise_binary("MISE-DOCTOR"));
}
fn detect(mise_shell: Option<&str>, shell_var: Option<&str>, fallback: &str) -> String {
detect_shell(
mise_shell.map(str::to_string),
shell_var.map(str::to_string),
fallback,
)
.map(|st| st.to_string())
.unwrap_or_else(|| "(none)".to_string())
}
#[test]
fn mise_shell_wins_and_an_unparseable_one_is_still_the_answer() {
assert_eq!(detect(Some("zsh"), Some("/bin/bash"), "/bin/bash"), "zsh");
assert_eq!(
detect(Some("nonsense"), Some("/bin/bash"), "/bin/bash"),
"(none)"
);
}
#[cfg(windows)]
#[test]
fn shell_is_consulted_on_windows() {
for shell_var in [
r"C:\Program Files\Git\bin\bash.exe",
"/bin/bash.exe",
r"C:\msys64\usr\bin\zsh.exe",
] {
let expected = match shell_var.contains("zsh") {
true => "zsh",
false => "bash",
};
assert_eq!(
detect(None, Some(shell_var), r"C:\WINDOWS\system32\cmd.exe"),
expected,
"{shell_var}"
);
}
assert_eq!(detect(None, None, r"C:\WINDOWS\system32\cmd.exe"), "(none)");
}
#[cfg(unix)]
#[test]
fn the_fallback_is_the_only_second_source_on_unix() {
assert_eq!(detect(None, Some("/bin/zsh"), "/bin/bash"), "bash");
assert_eq!(detect(None, None, "/bin/zsh"), "zsh");
assert_eq!(detect(None, None, "sh"), "bash");
}
#[test]
fn a_self_replace_copy_of_this_binary_is_recognised() {
let rand = "qzcdgqhxhqwhzdwyqchsmdxcqouxxche";
assert!(is_self_replace_helper(
&format!(".mise.{rand}.__selfdelete__.exe"),
"mise"
));
assert!(is_self_replace_helper(
&format!(".mise.{rand}.__relocated__.exe"),
"mise"
));
assert!(is_self_replace_helper(
&format!(".mise-dev.{rand}.__selfdelete__.exe"),
"mise-dev"
));
}
#[test]
fn ordinary_names_and_other_applications_are_not() {
let rand = "qzcdgqhxhqwhzdwyqchsmdxcqouxxche";
assert!(!is_self_replace_helper("mise.exe", "mise"));
assert!(!is_self_replace_helper("node.exe", "mise"));
assert!(!is_self_replace_helper(
&format!(".node.{rand}.__selfdelete__.exe"),
"mise"
));
assert!(!is_self_replace_helper(".mise.something.exe", "mise"));
}
#[test]
fn a_name_that_only_resembles_a_generated_one_is_left_alone() {
let ok = "qzcdgqhxhqwhzdwyqchsmdxcqouxxche";
assert_eq!(ok.len(), SELF_REPLACE_RANDOM_LEN, "premise");
assert!(is_self_replace_helper(
&format!(".mise.{ok}.__selfdelete__.exe"),
"mise"
));
assert!(!is_self_replace_helper(".mise..__selfdelete__.exe", "mise"));
assert!(!is_self_replace_helper(".mise.__selfdelete__.exe", "mise"));
assert!(!is_self_replace_helper(
".mise.abc.__selfdelete__.exe",
"mise"
));
assert!(!is_self_replace_helper(
&format!(".mise.{ok}x.__selfdelete__.exe"),
"mise"
));
let digits = format!("{}1", &ok[..SELF_REPLACE_RANDOM_LEN - 1]);
assert!(!is_self_replace_helper(
&format!(".mise.{digits}.__selfdelete__.exe"),
"mise"
));
let upper = format!("{}A", &ok[..SELF_REPLACE_RANDOM_LEN - 1]);
assert!(!is_self_replace_helper(
&format!(".mise.{upper}.__selfdelete__.exe"),
"mise"
));
}
#[cfg(windows)]
#[test]
fn an_orphan_from_a_differently_cased_launch_is_still_ours() {
let rand = "qzcdgqhxhqwhzdwyqchsmdxcqouxxche";
for stem in ["mise", "MISE", "Mise"] {
for written in ["mise", "MISE", "Mise"] {
assert!(
is_self_replace_helper(&format!(".{written}.{rand}.__selfdelete__.exe"), stem),
"stem {stem} should claim an orphan written as {written}"
);
}
}
assert!(!is_self_replace_helper(
&format!(".NODE.{rand}.__selfdelete__.exe"),
"mise"
));
}
#[cfg(unix)]
#[test]
fn casing_still_separates_names_on_unix() {
let rand = "qzcdgqhxhqwhzdwyqchsmdxcqouxxche";
assert!(is_self_replace_helper(
&format!(".mise.{rand}.__selfdelete__.exe"),
"mise"
));
assert!(!is_self_replace_helper(
&format!(".MISE.{rand}.__selfdelete__.exe"),
"mise"
));
}
}