use std::collections::BTreeMap;
use std::ffi::OsString;
use clap::ValueHint;
use duct::IntoExecutablePath;
#[cfg(not(any(test, windows)))]
use eyre::{Result, bail};
#[cfg(any(test, windows))]
use eyre::{Result, eyre};
use crate::cli::args::ToolArg;
#[cfg(any(test, windows))]
use crate::cmd;
use crate::config::{Config, Settings};
use crate::env;
use crate::prepare::{PrepareEngine, PrepareOptions};
use crate::sandbox::SandboxConfig;
use crate::toolset::env_cache::CachedEnv;
use crate::toolset::{InstallOptions, ResolveOptions, ToolsetBuilder};
#[derive(Debug, clap::Args)]
#[clap(visible_alias = "x", verbatim_doc_comment, after_long_help = AFTER_LONG_HELP)]
pub struct Exec {
#[clap(value_name = "TOOL@VERSION")]
pub tool: Vec<ToolArg>,
#[clap(conflicts_with = "c", required_unless_present = "c", last = true)]
pub command: Option<Vec<String>>,
#[clap(short, long = "command", value_hint = ValueHint::CommandString, conflicts_with = "command")]
pub c: Option<String>,
#[clap(long, short, env = "MISE_JOBS", verbatim_doc_comment)]
pub jobs: Option<usize>,
#[clap(long, value_name = "VAR", verbatim_doc_comment)]
pub allow_env: Vec<String>,
#[clap(long, value_name = "HOST", verbatim_doc_comment)]
pub allow_net: Vec<String>,
#[clap(long, value_name = "PATH", verbatim_doc_comment)]
pub allow_read: Vec<std::path::PathBuf>,
#[clap(long, value_name = "PATH", verbatim_doc_comment)]
pub allow_write: Vec<std::path::PathBuf>,
#[clap(long, verbatim_doc_comment)]
pub deny_all: bool,
#[clap(long, verbatim_doc_comment)]
pub deny_env: bool,
#[clap(long, verbatim_doc_comment)]
pub deny_net: bool,
#[clap(long, verbatim_doc_comment)]
pub deny_read: bool,
#[clap(long, verbatim_doc_comment)]
pub deny_write: bool,
#[clap(long)]
pub fresh_env: bool,
#[clap(long)]
pub no_prepare: bool,
#[clap(long, overrides_with = "jobs")]
pub raw: bool,
}
impl Exec {
#[async_backtrace::framed]
pub async fn run(self) -> eyre::Result<()> {
if self.fresh_env {
env::reset_env_cache_key();
}
let mut config = Config::get().await?;
let has_explicit_latest = self
.tool
.iter()
.any(|t| t.tvr.as_ref().is_some_and(|tvr| tvr.version() == "latest"));
let resolve_options = if has_explicit_latest {
ResolveOptions {
latest_versions: true,
use_locked_version: false,
..Default::default()
}
} else {
Default::default()
};
let mut ts = measure!("toolset", {
ToolsetBuilder::new()
.with_args(&self.tool)
.with_default_to_latest(true)
.with_resolve_options(resolve_options.clone())
.build(&config)
.await?
});
let opts = InstallOptions {
force: false,
jobs: self.jobs,
raw: self.raw,
missing_args_only: !self.tool.is_empty()
|| !Settings::get().exec_auto_install
|| *env::__MISE_SHIM,
skip_auto_install: !Settings::get().exec_auto_install || !Settings::get().auto_install,
resolve_options,
..Default::default()
};
let (_, missing) = measure!("install_arg_versions", {
ts.install_missing_versions(&mut config, &opts).await?
});
if has_explicit_latest {
ts.resolve_with_opts(&config, &opts.resolve_options).await?;
}
measure!("notify_if_versions_missing", {
ts.notify_missing_versions(missing);
});
let (program, mut args) = parse_command(&env::SHELL, &self.command, &self.c);
let mut env = measure!("env_with_path", { ts.env_with_path(&config).await? });
if !self.no_prepare {
let engine = PrepareEngine::new(&config)?;
engine
.run(PrepareOptions {
auto_only: true, env: env.clone(),
..Default::default()
})
.await?;
}
if !env::MISE_ENV.is_empty() {
env.insert("MISE_ENV".to_string(), env::MISE_ENV.join(","));
}
if Settings::get().env_cache && !self.fresh_env {
let key = CachedEnv::ensure_encryption_key();
env.insert("__MISE_ENV_CACHE_KEY".to_string(), key);
}
if program.rsplit('/').next() == Some("fish") {
let mut cmd = vec![];
for (k, v) in env.iter().filter(|(k, _)| *k != "PATH") {
cmd.push(format!(
"set -gx {} {}",
shell_escape::escape(k.into()),
shell_escape::escape(v.into())
));
}
let (_, env_results) = ts.final_env(&config).await?;
for p in ts.list_final_paths(&config, env_results).await? {
cmd.push(format!(
"fish_add_path -gm {}",
shell_escape::escape(p.to_string_lossy())
));
}
args.insert(0, cmd.join("\n"));
args.insert(0, "-C".into());
}
let mut sandbox = SandboxConfig {
deny_read: self.deny_all || self.deny_read,
deny_write: self.deny_all || self.deny_write,
deny_net: self.deny_all || self.deny_net,
deny_env: self.deny_all || self.deny_env,
allow_read: self.allow_read,
allow_write: self.allow_write,
allow_net: self.allow_net,
allow_env: self.allow_env,
};
sandbox.resolve_paths();
if sandbox.is_active() {
Settings::get().ensure_experimental("sandbox")?;
env = sandbox.filter_env(&env);
}
time!("exec");
exec_program(program, args, env, &sandbox).await
}
}
#[cfg(all(not(test), unix))]
pub async fn exec_program<T, U>(
program: T,
args: U,
env: BTreeMap<String, String>,
sandbox: &SandboxConfig,
) -> Result<()>
where
T: IntoExecutablePath,
U: IntoIterator,
U::Item: Into<OsString>,
{
if sandbox.effective_deny_env() {
for (k, _) in std::env::vars() {
if !env.contains_key(&k) {
env::remove_var(&k);
}
}
}
for (k, v) in env.iter() {
env::set_var(k, v);
}
let args = args.into_iter().map(Into::into).collect::<Vec<_>>();
let program = program.to_executable();
let program = if program.to_string_lossy().contains('/') {
program
} else {
let cwd = crate::dirs::CWD.clone().unwrap_or_default();
let lookup_path = env.get(&*env::PATH_KEY).map(|path_val| {
let user_shims = &*crate::dirs::SHIMS;
let sys_shims = crate::env::MISE_SYSTEM_DATA_DIR.join("shims");
let is_shims_dir = |p: &std::path::PathBuf| p == user_shims || p == &sys_shims;
let pristine: std::collections::HashSet<_> = crate::env::PATH.iter().collect();
let all_paths: Vec<_> = std::env::split_paths(&OsString::from(path_val)).collect();
let mise_added: Vec<_> = all_paths
.iter()
.filter(|p| !pristine.contains(p))
.cloned()
.collect();
let original: Vec<_> = all_paths
.iter()
.filter(|p| pristine.contains(p) && !is_shims_dir(p))
.cloned()
.collect();
std::env::join_paths(mise_added.iter().chain(original.iter())).unwrap()
});
match which::which_in(&program, lookup_path, cwd) {
Ok(resolved) => resolved.into_os_string(),
Err(_) => program, }
};
let args_str: Vec<String> = args
.iter()
.map(|a| a.to_string_lossy().into_owned())
.collect();
if let Some(sandboxed) = sandbox.apply(&program.to_string_lossy(), &args_str).await? {
let err = exec::Command::new(&sandboxed.program)
.args(&sandboxed.args)
.exec();
bail!("{} {err}", sandboxed.program);
}
let err = exec::Command::new(program.clone()).args(&args).exec();
bail!("{:?} {err}", program.to_string_lossy())
}
#[cfg(all(windows, not(test)))]
pub async fn exec_program<T, U>(
program: T,
args: U,
env: BTreeMap<String, String>,
sandbox: &SandboxConfig,
) -> Result<()>
where
T: IntoExecutablePath,
U: IntoIterator,
U::Item: Into<OsString>,
{
if sandbox.is_active() {
warn!("sandbox is not supported on Windows, running unsandboxed");
}
for (k, v) in env.iter() {
env::set_var(k, v);
}
let cwd = crate::dirs::CWD.clone().unwrap_or_default();
let program = program.to_executable();
let lookup_path = env.get(&*env::PATH_KEY).map(|path_val| {
let shims_normalized = crate::dirs::SHIMS
.to_string_lossy()
.to_lowercase()
.replace('/', "\\");
let sys_shims_normalized = crate::env::MISE_SYSTEM_DATA_DIR
.join("shims")
.to_string_lossy()
.to_lowercase()
.replace('/', "\\");
let is_shims = |p: &std::path::PathBuf| {
let expanded = crate::file::replace_path(p);
let normalized = expanded.to_string_lossy().to_lowercase().replace('/', "\\");
normalized == shims_normalized || normalized == sys_shims_normalized
};
let pristine: std::collections::HashSet<_> = crate::env::PATH
.iter()
.map(|p| {
crate::file::replace_path(p)
.to_string_lossy()
.to_lowercase()
.replace('/', "\\")
})
.collect();
let all_paths: Vec<_> = std::env::split_paths(&OsString::from(path_val)).collect();
let mise_added: Vec<_> = all_paths
.iter()
.filter(|p| {
let normalized = crate::file::replace_path(p)
.to_string_lossy()
.to_lowercase()
.replace('/', "\\");
!pristine.contains(&normalized)
})
.cloned()
.collect();
let original: Vec<_> = all_paths
.iter()
.filter(|p| {
let normalized = crate::file::replace_path(p)
.to_string_lossy()
.to_lowercase()
.replace('/', "\\");
pristine.contains(&normalized) && !is_shims(p)
})
.cloned()
.collect();
std::env::join_paths(mise_added.iter().chain(original.iter())).unwrap()
});
let program = which::which_in(program, lookup_path, cwd)?;
let cmd = cmd::cmd(program, args);
win_exec::set_ctrlc_handler()?;
let res = cmd.unchecked().run()?;
match res.status.code() {
Some(code) => {
std::process::exit(code);
}
None => Err(eyre!("command failed: terminated by signal")),
}
}
#[cfg(test)]
pub async fn exec_program<T, U>(
program: T,
args: U,
env: BTreeMap<String, String>,
_sandbox: &SandboxConfig,
) -> Result<()>
where
T: IntoExecutablePath,
U: IntoIterator,
U::Item: Into<OsString>,
{
let mut cmd = cmd::cmd(program, args);
for (k, v) in env.iter() {
cmd = cmd.env(k, v);
}
let res = cmd.unchecked().run()?;
match res.status.code() {
Some(0) => Ok(()),
Some(code) => Err(eyre!("command failed: exit code {}", code)),
None => Err(eyre!("command failed: terminated by signal")),
}
}
#[cfg(all(windows, not(test)))]
mod win_exec {
use eyre::{Result, eyre};
use winapi::shared::minwindef::{BOOL, DWORD, FALSE, TRUE};
use winapi::um::consoleapi::SetConsoleCtrlHandler;
unsafe extern "system" fn ctrlc_handler(_: DWORD) -> BOOL {
TRUE
}
pub(super) fn set_ctrlc_handler() -> Result<()> {
if unsafe { SetConsoleCtrlHandler(Some(ctrlc_handler), TRUE) } == FALSE {
Err(eyre!("Could not set Ctrl-C handler."))
} else {
Ok(())
}
}
}
fn parse_command(
shell: &str,
command: &Option<Vec<String>>,
c: &Option<String>,
) -> (String, Vec<String>) {
match (&command, &c) {
(Some(command), _) => {
let (program, args) = command.split_first().unwrap();
(program.clone(), args.into())
}
_ => (
shell.into(),
vec![env::SHELL_COMMAND_FLAG.into(), c.clone().unwrap()],
),
}
}
static AFTER_LONG_HELP: &str = color_print::cstr!(
r#"<bold><underline>Examples:</underline></bold>
$ <bold>mise exec node@20 -- node ./app.js</bold> # launch app.js using node-20.x
$ <bold>mise x node@20 -- node ./app.js</bold> # shorter alias
# Specify command as a string:
$ <bold>mise exec node@20 python@3.11 --command "node -v && python -V"</bold>
# Run a command in a different directory:
$ <bold>mise x -C /path/to/project node@20 -- node ./app.js</bold>
"#
);