use std::collections::HashSet;
use std::str::FromStr;
use std::sync::Arc;
use crate::cli::args::ToolArg;
use crate::config::Config;
use crate::config::Settings;
use crate::errors::split_install_result;
use crate::hooks::Hooks;
use crate::install_before::resolve_cli_minimum_release_age;
use crate::task::TaskLoadContext;
use crate::task::task_context_builder::TaskContextBuilder;
use crate::task::task_tool_installer::TaskToolInstaller;
use crate::toolset::{
InstallOptions, ResolveOptions, ToolRequest, ToolRequestSet, ToolSource, ToolVersionOptions,
Toolset, ensure_compatible_install_requests, tool_env_vars,
};
use crate::ui::install_progress::install_progress;
use crate::ui::multi_progress_report::MultiProgressReport;
use crate::ui::progress_report::ProgressIcon;
use crate::ui::resolve_progress;
use crate::{config, env, exit, hooks};
use eyre::Result;
use itertools::Itertools;
use jiff::Timestamp;
use path_absolutize::Absolutize;
use std::path::PathBuf;
#[derive(Debug, Default, usage_rs::Args)]
#[usage(
visible_alias = "i",
verbatim_doc_comment,
example(
r###"mise install node@20.0.0 # install a specific node version
mise install node@20 # install the latest node 20.x
mise install node # install the version specified in mise.toml
mise install # install everything specified in mise.toml
mise install --include-lazy # also install tools configured for lazy installation
mise install --include-task-tools # also install tools required by tasks"###
)
)]
pub(crate) struct Install {
#[usage(value_name = "TOOL@VERSION")]
tool: Option<Vec<ToolArg>>,
#[usage(long, short, verbatim_doc_comment)]
force: bool,
#[usage(long, short, env = "MISE_JOBS", verbatim_doc_comment)]
jobs: Option<usize>,
#[usage(long, short = 'n', verbatim_doc_comment)]
dry_run: bool,
#[usage(long, short, action = usage_rs::ArgAction::Count)]
verbose: u8,
#[usage(long, verbatim_doc_comment)]
dry_run_code: bool,
#[usage(long, verbatim_doc_comment)]
include_task_tools: bool,
#[usage(long, verbatim_doc_comment)]
include_lazy: bool,
#[usage(long, alias = "before", verbatim_doc_comment)]
minimum_release_age: Option<String>,
#[usage(long, env = "MISE_MONOREPO", verbatim_doc_comment)]
monorepo: bool,
#[usage(long, overrides = "jobs")]
raw: bool,
#[usage(long, verbatim_doc_comment, value_hint = ValueHint::DirPath, conflicts = "system")]
shared: Option<PathBuf>,
#[usage(long, verbatim_doc_comment, conflicts = "shared")]
system: bool,
#[usage(skip)]
yes: bool,
}
impl Install {
pub(crate) fn new_bare(dry_run: bool, yes: bool) -> Self {
Self {
dry_run,
yes,
..Default::default()
}
}
pub(super) fn is_dry_run(&self) -> bool {
self.dry_run || self.dry_run_code
}
#[async_backtrace::framed]
pub async fn run(self) -> Result<()> {
let config = Config::get().await?;
if !self.is_dry_run() {
crate::lockfile::migrate_monorepo_lockfiles(&config, false)?;
}
let task_requests = self.collect_task_tool_requests(&config).await?;
match &self.tool {
Some(runtime) => {
let original_tool_args = env::TOOL_ARGS.read().unwrap().clone();
env::TOOL_ARGS.write().unwrap().clone_from(runtime);
self.install_runtimes(config, runtime, original_tool_args, task_requests)
.await?
}
None => self.install_missing_runtimes(config, task_requests).await?,
};
self.hint_missing_system_packages().await;
Ok(())
}
async fn collect_task_tool_requests(&self, config: &Arc<Config>) -> Result<Vec<ToolRequest>> {
if !self.include_task_tools {
return Ok(vec![]);
}
let task_context = self.monorepo.then(TaskLoadContext::all);
let tasks = config.tasks_with_context(task_context.as_ref()).await?;
let context_builder = TaskContextBuilder::new();
let installer = TaskToolInstaller::new(&context_builder, &[]);
installer
.collect_tool_requests(config, tasks.values())
.await
}
async fn hint_missing_system_packages(&self) {
if !crate::hint::hint_would_display("system_packages_missing") {
return;
}
let Ok(config) = Config::get().await else {
return;
};
let mgrs = crate::system::packages_from_config(&config);
if mgrs.is_empty() {
return;
}
let mut available = std::collections::HashSet::new();
for mp in &mgrs {
if !mp.disabled && mp.manager.unavailable_reason_async().await.is_none() {
available.insert(mp.manager.name().to_string());
}
}
let fingerprint = mgrs
.iter()
.filter(|mp| available.contains(mp.manager.name()))
.flat_map(|mp| {
mp.requests
.iter()
.map(move |r| format!("{}:{}", mp.manager.name(), r))
})
.collect::<Vec<_>>()
.join("\n");
let checked = crate::dirs::STATE.join("system-packages-checked");
if checked
.metadata()
.ok()
.and_then(|m| m.modified().ok())
.and_then(|t| t.elapsed().ok())
.is_some_and(|age| age < std::time::Duration::from_secs(24 * 60 * 60))
&& crate::file::read_to_string(&checked).is_ok_and(|prev| prev == fingerprint)
{
return;
}
let mut missing = 0;
let mut all_queries_ok = true;
for mp in mgrs {
if !available.contains(mp.manager.name()) {
continue;
}
match mp.manager.installed(&mp.requests).await {
Ok(statuses) => {
missing += statuses
.iter()
.filter(|s| {
s.request.desired
== crate::system::packages::PackageDesiredState::Present
&& !s.state.is_installed()
&& !s.state.is_unavailable()
})
.count();
}
Err(_) => all_queries_ok = false,
}
}
if all_queries_ok {
let _ = crate::file::write(&checked, &fingerprint);
}
if missing > 0 {
hint!(
"system_packages_missing",
"{missing} system package(s) from [bootstrap.packages] are missing. Install them with",
"mise bootstrap packages apply"
);
}
}
#[async_backtrace::framed]
async fn install_runtimes(
&self,
config: Arc<Config>,
runtimes: &[ToolArg],
original_tool_args: Vec<ToolArg>,
task_requests: Vec<ToolRequest>,
) -> Result<()> {
let monorepo_union = if self.monorepo {
Some(config.monorepo_union().await?)
} else {
None
};
let mut install_config = self
.effective_config(&config, monorepo_union.as_ref())
.await?;
let base_trs = match &monorepo_union {
Some(union) => union.tool_request_set.clone(),
None => config.get_tool_request_set().await?.clone(),
};
let trs = extend_tool_request_set(base_trs.clone(), &task_requests);
let mut has_unmatched_wildcard = false;
let expanded_runtimes: Vec<ToolArg> = runtimes
.iter()
.flat_map(|ta| {
if let Some(backend_prefix) = ta.ba.short.strip_suffix(":*") {
let matching: Vec<_> = trs
.tools
.keys()
.filter(|ba| {
ba.short.starts_with(&format!("{backend_prefix}:"))
&& ba.tool_name != "*"
})
.filter_map(|ba| ToolArg::from_str(&ba.short).ok())
.collect();
if matching.is_empty() {
warn!("no tools found in config matching {}", ta.ba.short);
has_unmatched_wildcard = true;
}
return matching;
}
vec![ta.clone()]
})
.collect();
if expanded_runtimes.is_empty() && has_unmatched_wildcard {
return Ok(());
}
let tools: HashSet<String> = expanded_runtimes
.iter()
.map(|ta| ta.ba.short.clone())
.collect();
let configured_config_files = match &monorepo_union {
Some(union) => union.config_files.clone(),
None => config.config_files.clone(),
};
let configured_tools: HashSet<String> = configured_config_files
.values()
.filter_map(|cf| cf.to_tool_request_set().ok())
.flat_map(|cf_trs| cf_trs.tools.into_keys().map(|ba| ba.short.clone()))
.chain(tool_env_vars().map(|(name, _, _)| name))
.chain(task_requests.iter().map(|tr| tr.ba().short.clone()))
.collect();
let inactive_tools: Vec<String> = tools
.iter()
.filter(|t| !configured_tools.contains(*t))
.cloned()
.collect();
let mut ts: Toolset = trs.filter_by_tool(tools).into();
extend_toolset(&mut ts, &task_requests);
let mut tool_versions = self.get_requested_tool_versions(&ts, &expanded_runtimes)?;
tool_versions.extend(task_requests);
dedup_tool_requests(&mut tool_versions);
ensure_compatible_install_requests(&tool_versions)?;
let (mut versions, install_error) = if tool_versions.is_empty() {
warn!("no runtimes to install");
warn!("specify a version with `mise install <TOOL>@<VERSION>`");
(vec![], Ok(()))
} else {
if let Some(monorepo_union) = &monorepo_union {
Toolset::ensure_config_plugins_installed_from_urls(
&config,
&monorepo_union.repo_urls,
self.is_dry_run(),
)
.await?;
}
split_install_result(
ts.install_all_versions(&mut install_config, tool_versions, &self.install_opts()?)
.await,
)
};
let installed_shorts: HashSet<String> =
versions.iter().map(|tv| tv.short().to_string()).collect();
if self.is_dry_run() {
if self.dry_run_code {
let has_work = versions.iter().any(|tv| tv.install_satisfied != Some(true));
if has_work {
return Err(exit::request(1));
}
}
return install_error;
}
if install_error.is_ok() || !versions.is_empty() {
env::TOOL_ARGS
.write()
.unwrap()
.clone_from(&original_tool_args);
let config = Config::reset().await?;
let rebuild_config = self.effective_config(&config, None).await?;
let ts_owned;
let ts = if self.monorepo {
ts_owned =
Self::resolved_toolset_from_trs(&rebuild_config, base_trs.clone()).await?;
&ts_owned
} else {
rebuild_config.get_toolset().await?
};
let current_versions = ts.list_current_versions();
versions.retain(|tv| current_versions.iter().any(|(_, cv)| tv == cv));
config::rebuild_shims_and_runtime_symlinks(
&rebuild_config,
ts,
&versions,
crate::lockfile::LockfileUpdateMode::Normal,
)
.await?;
}
let inactive_tools: Vec<String> = inactive_tools
.into_iter()
.filter(|t| installed_shorts.contains(t))
.collect();
if !inactive_tools.is_empty() {
let tool_list = inactive_tools.join(", ");
let use_cmds: Vec<String> = inactive_tools
.iter()
.map(|t| format!(" mise use {t}"))
.collect();
warn!(
"{tool_list} installed but not activated — {} not in any config file.\nTo install and activate, run:\n{}",
if inactive_tools.len() == 1 {
"it is"
} else {
"they are"
},
use_cmds.join("\n"),
);
}
install_error?;
Ok(())
}
fn install_opts(&self) -> Result<InstallOptions> {
let install_dir = if self.system {
Some(Settings::get().system_installs_dir().to_path_buf())
} else {
self.shared.clone()
}
.map(|path| path.absolutize().map(|path| path.into_owned()))
.transpose()?;
Ok(InstallOptions {
force: self.force,
jobs: self.jobs,
raw: self.raw,
missing_args_only: false,
include_lazy: self.tool.is_some() || self.include_lazy,
resolve_options: ResolveOptions {
use_locked_version: true,
latest_versions: true,
resolve_rolling_channels: false,
prefer_exact_version: false,
before_date: self.get_before_date()?,
before_date_from_default: false,
filter_installed_versions_by_release_date: false,
offline: false,
refresh_remote_versions: false,
inactive: false,
},
dry_run: self.is_dry_run(),
locked: Settings::get().locked,
install_dir,
yes: self.yes || Settings::get().yes,
..Default::default()
})
}
fn get_before_date(&self) -> Result<Option<Timestamp>> {
resolve_cli_minimum_release_age(self.minimum_release_age.as_deref())
}
fn get_requested_tool_versions(
&self,
ts: &Toolset,
runtimes: &[ToolArg],
) -> Result<Vec<ToolRequest>> {
let mut requests = vec![];
for ta in ToolArg::double_tool_condition(runtimes)? {
match ta.tvr {
Some(tv) => requests.push(tv),
None => {
match ts.versions.get(ta.ba.as_ref()) {
Some(tvl) => {
for tvr in &tvl.requests {
requests.push(tvr.clone());
}
}
None => {
if self.monorepo {
continue;
}
let tvr =
ToolRequest::new(ta.ba.clone(), "latest", ToolSource::Argument)?;
requests.push(tvr);
}
}
}
}
}
Ok(requests)
}
async fn install_missing_runtimes(
&self,
config: Arc<Config>,
task_requests: Vec<ToolRequest>,
) -> eyre::Result<()> {
let monorepo_union = if self.monorepo {
Some(config.monorepo_union().await?)
} else {
None
};
let base_trs = measure!("get_tool_request_set", {
match &monorepo_union {
Some(union) => union.tool_request_set.clone(),
None => config.get_tool_request_set().await?.clone(),
}
});
let trs = extend_tool_request_set(base_trs.clone(), &task_requests);
let install_candidates = trs
.tools
.values()
.flatten()
.filter(|tr| tr.is_os_supported() && !matches!(tr, ToolRequest::System { .. }))
.cloned()
.collect_vec();
ensure_compatible_install_requests(&install_candidates)?;
let mut install_config = self
.effective_config(&config, monorepo_union.as_ref())
.await?;
if let Some(monorepo_union) = &monorepo_union {
Toolset::ensure_config_plugins_installed_from_urls(
&config,
&monorepo_union.repo_urls,
self.is_dry_run(),
)
.await?;
} else {
Toolset::ensure_config_plugins_installed(&config, self.is_dry_run()).await?;
}
for ba in &trs.unknown_tools {
ba.backend()?;
}
let opts = self.install_opts()?;
let install_candidates = install_candidates
.into_iter()
.filter(|tr| self.include_lazy || tr.options().lazy != Some(true))
.collect_vec();
let mut progress = (!opts.raw && !opts.dry_run)
.then(|| {
install_progress(
&MultiProgressReport::get(),
install_candidates
.iter()
.map(|tr| (tr.to_string(), tr.to_string())),
)
})
.flatten();
let requests = measure!("fetching install runtimes", {
if self.force {
install_candidates
} else {
let mut missing = vec![];
for tr in install_candidates {
let reporter = progress
.as_ref()
.and_then(|p| p.start_tool(&tr.to_string()));
let satisfied = resolve_progress::scope(
reporter.as_ref().map(|p| p.reporter()),
tr.is_install_satisfied(&install_config),
)
.await;
if satisfied {
if let Some(reporter) = reporter {
reporter.finish_with_icon(
"already installed".into(),
ProgressIcon::Skipped,
);
reporter.complete(None);
}
} else {
if let Some(progress) = &progress {
progress.queue_tool(&tr.to_string());
}
missing.push(tr);
}
}
missing
}
});
let has_work = !requests.is_empty();
let (versions, install_error) = if requests.is_empty() {
if let Some(mut progress) = progress.take() {
progress.finish(vec![]);
}
measure!("run_postinstall_hook", {
info!("all tools are installed");
let ts_owned;
let ts = if self.is_dry_run() {
ts_owned = Toolset::from(base_trs.clone());
&ts_owned
} else if self.monorepo || self.include_task_tools {
ts_owned =
Self::resolved_toolset_from_trs(&install_config, trs.clone()).await?;
&ts_owned
} else {
install_config.get_toolset().await?
};
hooks::run_one_hook_with_context(
&install_config,
ts,
Hooks::Postinstall,
None,
Some(&[]),
self.is_dry_run(),
false,
)
.await;
(vec![], Ok(()))
})
} else {
let mut ts = Toolset::from(trs.clone());
measure!("install_all_versions", {
split_install_result(
ts.install_all_versions_with_progress(
&mut install_config,
requests,
&opts,
progress,
)
.await,
)
})
};
if self.is_dry_run() {
if self.dry_run_code && has_work {
return Err(exit::request(1));
}
return install_error;
}
if install_error.is_ok() || !versions.is_empty() {
measure!("rebuild_shims_and_runtime_symlinks", {
let rebuild_config = self.effective_config(&install_config, None).await?;
let ts_owned;
let ts = if self.monorepo {
ts_owned =
Self::resolved_toolset_from_trs(&rebuild_config, base_trs.clone()).await?;
&ts_owned
} else {
rebuild_config.get_toolset().await?
};
let current_versions = ts.list_current_versions();
let versions = versions
.iter()
.filter(|tv| current_versions.iter().any(|(_, current)| *tv == current))
.cloned()
.collect::<Vec<_>>();
config::rebuild_shims_and_runtime_symlinks(
&rebuild_config,
ts,
&versions,
crate::lockfile::LockfileUpdateMode::Normal,
)
.await?;
});
}
install_error?;
Ok(())
}
async fn effective_config(
&self,
config: &Arc<Config>,
monorepo_union: Option<&config::MonorepoUnion>,
) -> Result<Arc<Config>> {
if !self.monorepo {
return Ok(config.clone());
}
let config_files = match monorepo_union {
Some(union) => union.config_files.clone(),
None => config.monorepo_union().await?.config_files,
};
Ok(config.with_config_files(config_files))
}
async fn resolved_toolset_from_trs(
config: &Arc<Config>,
trs: ToolRequestSet,
) -> Result<Toolset> {
let mut ts: Toolset = trs.into();
ts.resolve(config).await?;
Ok(ts)
}
}
type ToolRequestIdentity = (String, String, ToolVersionOptions);
fn tool_request_identity(request: &ToolRequest) -> ToolRequestIdentity {
(request.ba().full(), request.version(), request.options())
}
fn extend_tool_request_set(
mut requests: ToolRequestSet,
additional: &[ToolRequest],
) -> ToolRequestSet {
let mut seen = requests
.tools
.values()
.flatten()
.map(tool_request_identity)
.collect::<HashSet<_>>();
for request in additional {
if seen.insert(tool_request_identity(request)) {
requests.add_version(request.clone(), request.source());
}
}
requests
}
fn dedup_tool_requests(requests: &mut Vec<ToolRequest>) {
let mut seen = HashSet::new();
requests.retain(|request| seen.insert(tool_request_identity(request)));
}
fn extend_toolset(toolset: &mut Toolset, additional: &[ToolRequest]) {
let mut seen = toolset
.versions
.values()
.flat_map(|versions| &versions.requests)
.map(tool_request_identity)
.collect::<HashSet<_>>();
for request in additional {
if seen.insert(tool_request_identity(request)) {
toolset.add_version(request.clone());
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::args::BackendArg;
use crate::toolset::parse_tool_options;
fn request(version: &str, options: &str, source: ToolSource) -> ToolRequest {
ToolRequest::new_with_options(
Arc::new(BackendArg::new(
"tiny".to_string(),
Some("asdf:mise-plugins/mise-tiny".to_string()),
)),
version,
parse_tool_options(options),
source,
)
.unwrap()
}
#[test]
fn task_request_union_deduplicates_identical_requests_across_sources() {
let configured = request("1.0.0", "", ToolSource::Argument);
let task = request(
"1.0.0",
"",
ToolSource::MiseToml(PathBuf::from("mise.toml")),
);
let mut requests = ToolRequestSet::new();
requests.add_version(configured.clone(), configured.source());
let requests = extend_tool_request_set(requests, &[task]);
assert_eq!(requests.tools.values().flatten().count(), 1);
}
#[test]
fn task_request_union_preserves_versions_and_options() {
let configured = request("1.0.0", "flavor=one", ToolSource::Argument);
let task_version = request("2.0.0", "flavor=one", ToolSource::Argument);
let task_options = request("1.0.0", "flavor=two", ToolSource::Argument);
let mut requests = ToolRequestSet::new();
requests.add_version(configured.clone(), configured.source());
let requests = extend_tool_request_set(requests, &[task_version, task_options]);
assert_eq!(requests.tools.values().flatten().count(), 3);
}
}