use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use crate::backend::static_helpers::lookup_platform_key;
use crate::config::Config;
use crate::dirs;
use crate::file;
use crate::hash;
use crate::lockfile::PlatformInfo;
use crate::toolset::{InstallOptions, ToolRequest, ToolSource, ToolVersionOptions};
use clap::Parser;
use color_eyre::eyre::{Result, bail, eyre};
use eyre::ensure;
use serde::{Deserialize, Deserializer};
use toml::Value;
#[derive(Debug, Deserialize)]
pub struct ToolStubFile {
#[serde(default = "default_version")]
pub version: String,
pub bin: Option<String>, pub tool: Option<String>, #[serde(default)]
pub install_env: indexmap::IndexMap<String, String>,
#[serde(default)]
pub os: Option<Vec<String>>,
pub lock: Option<ToolStubLock>,
#[serde(flatten, deserialize_with = "deserialize_tool_stub_options")]
pub opts: indexmap::IndexMap<String, String>,
#[serde(skip)]
pub tool_name: String,
}
#[derive(Debug, Deserialize)]
pub struct ToolStubLock {
pub platforms: BTreeMap<String, ToolStubLockPlatform>,
}
#[derive(Debug, Deserialize)]
pub struct ToolStubLockPlatform {
pub url: Option<String>,
pub checksum: Option<String>,
}
fn deserialize_tool_stub_options<'de, D>(
deserializer: D,
) -> Result<indexmap::IndexMap<String, String>, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::Error;
let value = Value::deserialize(deserializer)?;
let mut opts = indexmap::IndexMap::new();
if let Value::Table(table) = value {
for (key, val) in table {
if matches!(
key.as_str(),
"version" | "bin" | "tool" | "install_env" | "os" | "lock"
) {
continue;
}
let string_value = match val {
Value::String(s) => s,
Value::Table(_) | Value::Array(_) => {
toml::to_string(&val).map_err(D::Error::custom)?
}
Value::Integer(i) => i.to_string(),
Value::Float(f) => f.to_string(),
Value::Boolean(b) => b.to_string(),
Value::Datetime(dt) => dt.to_string(),
};
opts.insert(key, string_value);
}
}
Ok(opts)
}
fn default_version() -> String {
"latest".to_string()
}
fn has_http_backend_config(opts: &indexmap::IndexMap<String, String>) -> bool {
if opts.contains_key("url") {
return true;
}
for (key, value) in opts {
if key.starts_with("platforms") && value.contains("url") {
return true;
}
}
false
}
fn extract_toml_from_bootstrap(content: &str) -> Option<String> {
let start_marker = "# MISE_TOOL_STUB:";
let end_marker = "# :MISE_TOOL_STUB";
let start_pos = content.find(start_marker)?;
let end_pos = content.find(end_marker)?;
if start_pos >= end_pos {
return None;
}
let between = &content[start_pos + start_marker.len()..end_pos];
let toml = between
.lines()
.map(|line| line.strip_prefix("# ").unwrap_or(line))
.collect::<Vec<_>>()
.join("\n");
Some(toml.trim().to_string())
}
impl ToolStubFile {
pub fn from_file(path: &Path) -> Result<Self> {
let content = file::read_to_string(path)?;
let stub_name = path
.file_name()
.and_then(|name| name.to_str())
.ok_or_else(|| eyre!("Invalid stub file name"))?
.to_string();
let toml_content = if let Some(toml) = extract_toml_from_bootstrap(&content) {
toml
} else {
content
};
let mut stub: ToolStubFile = toml::from_str(&toml_content)?;
let tool_name = stub
.tool
.clone()
.or_else(|| stub.opts.get("tool").map(|s| s.to_string()))
.unwrap_or_else(|| {
if has_http_backend_config(&stub.opts) {
format!("http:{stub_name}")
} else {
stub_name.to_string()
}
});
if stub.bin.is_none() {
stub.bin = Some(stub_name.to_string());
}
stub.tool_name = tool_name;
Ok(stub)
}
pub fn to_tool_request(&self, stub_path: &Path) -> Result<ToolRequest> {
use crate::cli::args::BackendArg;
let mut backend_arg = BackendArg::from(&self.tool_name);
let source = ToolSource::ToolStub(stub_path.to_path_buf());
let mut opts = self.opts.clone();
opts.shift_remove("tool");
if let Some(bin) = &self.bin {
opts.insert("bin".to_string(), bin.clone());
}
let options = ToolVersionOptions {
os: self.os.clone(),
install_env: self.install_env.clone(),
opts: opts.clone(),
};
backend_arg.set_opts(Some(options.clone()));
let version = if self.tool_name.starts_with("http:") && self.version == "latest" {
if let Some(url) =
lookup_platform_key(&options, "url").or_else(|| opts.get("url").cloned())
{
let checksum = lookup_platform_key(&options, "checksum")
.or_else(|| opts.get("checksum").cloned())
.unwrap_or_default();
let hash_input = format!("{url}:{checksum}");
format!("url-{}", &hash::hash_to_str(&hash_input)[..8])
} else {
self.version.clone()
}
} else {
self.version.clone()
};
ToolRequest::new_opts(backend_arg.into(), &version, options, source)
}
}
struct BinPathCache;
impl BinPathCache {
fn cache_key(stub_path: &Path) -> Result<String> {
let path_str = stub_path.to_string_lossy();
let mtime = stub_path.metadata()?.modified()?;
let mtime_str = format!(
"{:?}",
mtime
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
);
Ok(hash::hash_to_str(&format!("{path_str}:{mtime_str}")))
}
fn cache_file_path(cache_key: &str) -> PathBuf {
dirs::CACHE.join("tool-stubs").join(cache_key)
}
fn load(cache_key: &str) -> Option<PathBuf> {
let cache_path = Self::cache_file_path(cache_key);
if !cache_path.exists() {
return None;
}
match file::read_to_string(&cache_path) {
Ok(content) => {
let bin_path = PathBuf::from(content.trim());
if bin_path.exists() {
Some(bin_path)
} else {
let _ = file::remove_file(&cache_path);
None
}
}
Err(_) => None,
}
}
fn save(bin_path: &Path, cache_key: &str) -> Result<()> {
let cache_path = Self::cache_file_path(cache_key);
if let Some(parent) = cache_path.parent() {
file::create_dir_all(parent)?;
}
file::write(&cache_path, bin_path.to_string_lossy().as_bytes())?;
Ok(())
}
}
fn find_tool_version(
toolset: &crate::toolset::Toolset,
config: &std::sync::Arc<Config>,
tool_name: &str,
) -> Option<crate::toolset::ToolVersion> {
for (_backend, tv) in toolset.list_current_installed_versions(config) {
if tv.ba().short == tool_name {
return Some(tv);
}
}
None
}
fn find_single_subdirectory(install_path: &Path) -> Option<PathBuf> {
let Ok(entries) = std::fs::read_dir(install_path) else {
return None;
};
let dirs: Vec<_> = entries
.filter_map(|entry| entry.ok())
.filter(|entry| entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false))
.collect();
if dirs.len() == 1 {
Some(dirs[0].path())
} else {
None
}
}
fn try_find_bin_in_path(base_path: &Path, bin: &str) -> Option<PathBuf> {
let bin_path = base_path.join(bin);
if bin_path.exists() && crate::file::is_executable(&bin_path) {
Some(bin_path)
} else {
None
}
}
fn list_executable_files(dir_path: &Path) -> Vec<String> {
list_executable_files_recursive(dir_path, dir_path)
}
fn list_executable_files_recursive(base_path: &Path, current_path: &Path) -> Vec<String> {
let Ok(entries) = std::fs::read_dir(current_path) else {
return Vec::new();
};
let mut result = Vec::new();
for entry in entries.filter_map(|e| e.ok()) {
let path = entry.path();
let filename = entry.file_name();
let filename_str = filename.to_string_lossy();
if filename_str.starts_with('.') {
continue;
}
if path.is_dir() {
let subdir_files = list_executable_files_recursive(base_path, &path);
result.extend(subdir_files);
} else if path.is_file() && crate::file::is_executable(&path) {
if let Ok(relative_path) = path.strip_prefix(base_path) {
result.push(relative_path.to_string_lossy().to_string());
}
}
}
result
}
fn resolve_bin_with_path(
toolset: &crate::toolset::Toolset,
config: &std::sync::Arc<Config>,
bin: &str,
tool_name: &str,
) -> Option<PathBuf> {
let tv = find_tool_version(toolset, config, tool_name)?;
let install_path = tv.install_path();
if let Some(bin_path) = try_find_bin_in_path(&install_path, bin) {
return Some(bin_path);
}
let subdir_path = find_single_subdirectory(&install_path)?;
try_find_bin_in_path(&subdir_path, bin)
}
async fn resolve_bin_simple(
toolset: &crate::toolset::Toolset,
config: &std::sync::Arc<Config>,
bin: &str,
) -> Result<Option<PathBuf>> {
if let Some((backend, tv)) = toolset.which(config, bin).await {
backend.which(config, &tv, bin).await
} else {
Ok(None)
}
}
fn is_bin_path(bin: &str) -> bool {
bin.contains('/') || bin.contains('\\')
}
#[derive(Debug)]
enum BinPathError {
ToolNotFound(String),
BinNotFound {
tool_name: String,
bin: String,
available_bins: Vec<String>,
},
}
fn resolve_platform_specific_bin(stub: &ToolStubFile, stub_path: &Path) -> String {
let platform_key = get_current_platform_key();
let platform_bin_key = format!("platforms.{platform_key}.bin");
if let Some(platform_bin) = stub.opts.get(&platform_bin_key) {
return platform_bin.to_string();
}
if let Some(bin) = &stub.bin {
return bin.to_string();
}
stub_path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or(&stub.tool_name)
.to_string()
}
fn get_current_platform_key() -> String {
use crate::config::Settings;
let settings = Settings::get();
format!("{}-{}", settings.os(), settings.arch())
}
async fn find_cached_or_resolve_bin_path(
toolset: &crate::toolset::Toolset,
config: &std::sync::Arc<Config>,
stub: &ToolStubFile,
stub_path: &Path,
) -> Result<Result<PathBuf, BinPathError>> {
let cache_key = BinPathCache::cache_key(stub_path)?;
if let Some(bin_path) = BinPathCache::load(&cache_key) {
return Ok(Ok(bin_path));
}
let bin = resolve_platform_specific_bin(stub, stub_path);
let bin_path = if is_bin_path(&bin) {
resolve_bin_with_path(toolset, config, &bin, &stub.tool_name)
} else {
resolve_bin_simple(toolset, config, &bin).await?
};
if let Some(bin_path) = bin_path {
if let Err(e) = BinPathCache::save(&bin_path, &cache_key) {
crate::warn!("Failed to cache binary path: {e}");
}
return Ok(Ok(bin_path));
}
if is_bin_path(&bin) {
if let Some(tv) = find_tool_version(toolset, config, &stub.tool_name) {
let install_path = tv.install_path();
let available_bins = list_executable_files(&install_path);
Ok(Err(BinPathError::BinNotFound {
tool_name: stub.tool_name.clone(),
bin: bin.to_string(),
available_bins,
}))
} else {
Ok(Err(BinPathError::ToolNotFound(stub.tool_name.clone())))
}
} else {
if let Some(tv) = find_tool_version(toolset, config, &stub.tool_name) {
let available_bins = list_executable_files(&tv.install_path());
Ok(Err(BinPathError::BinNotFound {
tool_name: stub.tool_name.clone(),
bin: bin.to_string(),
available_bins,
}))
} else {
Ok(Err(BinPathError::ToolNotFound(stub.tool_name.clone())))
}
}
}
async fn execute_with_tool_request(
stub: &ToolStubFile,
config: &mut std::sync::Arc<Config>,
args: Vec<String>,
stub_path: &Path,
) -> Result<()> {
let tool_request = stub.to_tool_request(stub_path)?;
let source = ToolSource::ToolStub(stub_path.to_path_buf());
let mut toolset = crate::toolset::Toolset::new(source);
toolset.add_version(tool_request);
toolset.resolve(config).await?;
if let Some(lock) = &stub.lock {
for (_ba, tvl) in toolset.versions.iter_mut() {
for tv in &mut tvl.versions {
for (platform_key, lock_platform) in &lock.platforms {
let pi = PlatformInfo {
url: lock_platform.url.clone(),
checksum: lock_platform.checksum.clone(),
..Default::default()
};
tv.lock_platforms.insert(platform_key.clone(), pi);
}
}
}
}
ensure!(
!toolset.list_current_versions().is_empty(),
"No current versions found after resolving toolset"
);
let install_opts = InstallOptions {
force: false,
jobs: None,
raw: false,
missing_args_only: false,
resolve_options: Default::default(),
..Default::default()
};
let (_, missing) = toolset
.install_missing_versions(config, &install_opts)
.await?;
toolset.notify_missing_versions(missing);
match find_cached_or_resolve_bin_path(&toolset, &*config, stub, stub_path).await? {
Ok(bin_path) => {
let mut env = toolset.env_with_path(config).await?;
let mut path_env = crate::path_env::PathEnv::from_iter(crate::env::PATH.clone());
for p in toolset.list_paths(config).await {
path_env.add(p);
}
if let Some((backend, _tv)) = toolset.list_current_installed_versions(config).first() {
let btp = backend
.dependency_toolset(config)
.await?
.list_paths(config)
.await;
for p in btp {
path_env.add(p);
}
}
env.insert(crate::env::PATH_KEY.to_string(), path_env.to_string());
crate::cli::exec::exec_program(bin_path, args, env)
}
Err(e) => match e {
BinPathError::ToolNotFound(tool_name) => {
bail!("Tool '{}' not found", tool_name);
}
BinPathError::BinNotFound {
tool_name,
bin,
available_bins,
} => {
if available_bins.is_empty() {
bail!(
"Tool '{}' does not have an executable named '{}'",
tool_name,
bin
);
} else {
bail!(
"Tool '{}' does not have an executable named '{}'. Available executables: {}",
tool_name,
bin,
available_bins.join(", ")
);
}
}
},
}
}
#[derive(Debug, Parser)]
#[clap(disable_help_flag = true, disable_version_flag = true)]
pub struct ToolStub {
#[clap(value_name = "FILE")]
pub file: PathBuf,
#[clap(trailing_var_arg = true, allow_hyphen_values = true)]
pub args: Vec<String>,
}
impl ToolStub {
pub async fn run(self) -> Result<()> {
let file_str = self.file.to_string_lossy();
let args = {
let global_args = crate::env::ARGS.read().unwrap();
let file_str_ref: &str = file_str.as_ref();
if let Some(file_pos) = global_args.iter().position(|arg| arg == file_str_ref) {
global_args.get(file_pos + 1..).unwrap_or(&[]).to_vec()
} else {
vec![]
}
};
let stub = ToolStubFile::from_file(&self.file)?;
let mut config = Config::get().await?;
return execute_with_tool_request(&stub, &mut config, args, &self.file).await;
}
}
pub(crate) async fn short_circuit_stub(args: &[String]) -> Result<()> {
if args.is_empty() {
return Ok(());
}
let potential_stub_path = std::path::Path::new(&args[0]);
if !potential_stub_path.exists() {
return Ok(());
}
let cache_key = BinPathCache::cache_key(potential_stub_path)?;
if let Some(bin_path) = BinPathCache::load(&cache_key) {
let args = args[1..].to_vec();
return crate::cli::exec::exec_program(bin_path, args, BTreeMap::new());
}
Ok(())
}