use std::collections::HashSet;
use std::sync::Arc;
use crate::backend::pipx::PIPXBackend;
use crate::cli::args::{BackendArg, ToolArg};
use crate::config::{Config, config_file};
use crate::duration::parse_into_timestamp;
use crate::file::display_path;
use crate::toolset::outdated_info::OutdatedInfo;
use crate::toolset::{
ConfigScope, InstallOptions, ResolveOptions, ToolSource, ToolVersion, ToolsetBuilder,
get_versions_needed_by_tracked_configs_excluding_locks,
};
use crate::ui::multi_progress_report::MultiProgressReport;
use crate::ui::progress_report::SingleReport;
use crate::{config, exit, runtime_symlinks, ui};
use console::Term;
use demand::DemandOption;
use eyre::{Context, Result, eyre};
use jiff::Timestamp;
#[derive(Debug, clap::Args)]
#[clap(visible_alias = "up", verbatim_doc_comment, after_long_help = AFTER_LONG_HELP)]
pub struct Upgrade {
#[clap(value_name = "INSTALLED_TOOL@VERSION", verbatim_doc_comment)]
tool: Vec<ToolArg>,
#[clap(long, short, verbatim_doc_comment, conflicts_with = "tool")]
interactive: bool,
#[clap(long, short, env = "MISE_JOBS", verbatim_doc_comment)]
jobs: Option<usize>,
#[clap(long, short = 'l', verbatim_doc_comment)]
bump: bool,
#[clap(long, short = 'n', verbatim_doc_comment)]
dry_run: bool,
#[clap(long, short = 'x', value_name = "INSTALLED_TOOL", verbatim_doc_comment)]
exclude: Vec<ToolArg>,
#[clap(long, verbatim_doc_comment)]
dry_run_code: bool,
#[clap(long, verbatim_doc_comment, conflicts_with = "local")]
inactive: bool,
#[clap(long, verbatim_doc_comment)]
local: bool,
#[clap(long, alias = "before", verbatim_doc_comment)]
minimum_release_age: Option<String>,
#[clap(long, overrides_with = "jobs")]
raw: bool,
}
impl Upgrade {
fn is_dry_run(&self) -> bool {
self.dry_run || self.dry_run_code
}
fn scope(&self) -> ConfigScope {
if self.local {
ConfigScope::LocalOnly
} else {
ConfigScope::All
}
}
pub async fn run(self) -> Result<()> {
let mut config = Config::get().await?;
let ts = ToolsetBuilder::new()
.with_args(&self.tool)
.with_scope(self.scope())
.build(&config)
.await?;
let before_date = self.get_before_date()?;
let opts = ResolveOptions {
use_locked_version: false,
latest_versions: true,
before_date,
offline: false,
refresh_remote_versions: false,
inactive: self.inactive,
};
let filter_tools = if !self.interactive && !self.tool.is_empty() {
Some(self.tool.as_slice())
} else {
None
};
let exclude_tools = if !self.exclude.is_empty() {
Some(self.exclude.as_slice())
} else {
None
};
let mut outdated = ts
.list_outdated_versions_filtered(&config, self.bump, &opts, filter_tools, exclude_tools)
.await;
if self.interactive && !outdated.is_empty() {
outdated = self.get_interactive_tool_set(&outdated)?;
}
if outdated.is_empty() {
info!("All tools are up to date");
if !self.bump {
hint!(
"outdated_bump",
r#"By default, `mise upgrade` only upgrades versions that match your config. Use `mise upgrade --bump` to upgrade all new versions."#,
""
);
}
} else {
self.upgrade(&mut config, outdated, before_date).await?;
}
Ok(())
}
async fn upgrade(
&self,
config: &mut Arc<Config>,
outdated: Vec<OutdatedInfo>,
before_date: Option<Timestamp>,
) -> Result<()> {
let mpr = MultiProgressReport::get();
let mut ts = ToolsetBuilder::new()
.with_args(&self.tool)
.with_scope(self.scope())
.build(config)
.await?;
let mut outdated_with_config_files: Vec<(&OutdatedInfo, Arc<dyn config_file::ConfigFile>)> =
vec![];
for o in outdated.iter() {
if let (Some(path), Some(_bump)) = (o.source.path(), &o.bump) {
match config_file::parse(path).await {
Ok(cf) => outdated_with_config_files.push((o, cf)),
Err(e) => warn!("failed to parse {}: {e}", display_path(path)),
}
}
}
let config_file_updates = outdated_with_config_files
.iter()
.filter(|(o, cf)| {
if let Ok(trs) = cf.to_tool_request_set()
&& let Some(versions) = trs.tools.get(o.tool_request.ba())
&& versions.len() != 1
{
warn!("upgrading multiple versions with --bump is not yet supported");
return false;
}
true
})
.collect::<Vec<_>>();
let to_remove: Vec<_> = outdated
.iter()
.filter_map(|o| {
o.current.as_ref().and_then(|current| {
if &o.latest == current {
return None;
}
Some((o, current.clone()))
})
})
.collect();
if self.is_dry_run() {
for (o, current) in &to_remove {
miseprintln!("Would uninstall {}@{}", o.name, current);
}
for o in &outdated {
miseprintln!("Would install {}@{}", o.name, o.latest);
}
for (o, cf) in &config_file_updates {
miseprintln!(
"Would bump {}@{} in {}",
o.name,
o.tool_request.version(),
display_path(cf.get_path())
);
}
if !self.bump {
use crate::toolset::outdated_info::compute_config_bumps;
let tool_versions: Vec<(String, String)> = self
.tool
.iter()
.filter_map(|t| {
t.tvr
.as_ref()
.map(|tvr| (t.ba.short.clone(), tvr.version()))
})
.collect();
let refs: Vec<(&str, &str)> = tool_versions
.iter()
.map(|(n, v)| (n.as_str(), v.as_str()))
.collect();
let bumps = compute_config_bumps(config, &refs);
for bump in &bumps {
miseprintln!(
"Would update {} from {} to {} in {}",
bump.tool_name,
bump.old_version,
bump.new_version,
display_path(&bump.config_path)
);
}
}
if self.dry_run_code {
exit::exit(1);
}
return Ok(());
}
let opts = InstallOptions {
reason: "upgrade".to_string(),
force: false,
jobs: self.jobs,
raw: self.raw,
resolve_options: ResolveOptions {
use_locked_version: false,
latest_versions: true,
before_date,
offline: false,
refresh_remote_versions: false,
inactive: self.inactive,
},
locked: false,
..Default::default()
};
let tool_requests: Vec<_> = outdated.iter().map(|o| o.tool_request.clone()).collect();
let (mut successful_versions, install_error) =
match ts.install_all_versions(config, tool_requests, &opts).await {
Ok(versions) => (versions, eyre::Result::Ok(())),
Err(e) => match e.downcast_ref::<crate::errors::Error>() {
Some(crate::errors::Error::InstallFailed {
successful_installations,
..
}) => (successful_installations.clone(), eyre::Result::Err(e)),
_ => (vec![], eyre::Result::Err(e)),
},
};
for (o, cf) in config_file_updates {
if successful_versions
.iter()
.any(|v| v.ba() == o.tool_version.ba())
{
if let Err(e) =
cf.replace_versions(o.tool_request.ba(), vec![o.tool_request.clone()])
{
return Err(eyre!("Failed to update config for {}: {}", o.name, e));
}
if let Err(e) = cf.save() {
return Err(eyre!("Failed to save config for {}: {}", o.name, e));
}
}
}
if !self.bump {
use crate::toolset::outdated_info::{apply_config_bumps, compute_config_bumps};
let tool_versions: Vec<(String, String)> = self
.tool
.iter()
.filter_map(|t| {
t.tvr.as_ref().and_then(|tvr| {
let name = t.ba.short.clone();
if successful_versions.iter().any(|v| v.ba().short == name) {
Some((name, tvr.version()))
} else {
None
}
})
})
.collect();
let refs: Vec<(&str, &str)> = tool_versions
.iter()
.map(|(n, v)| (n.as_str(), v.as_str()))
.collect();
let bumps = compute_config_bumps(config, &refs);
apply_config_bumps(config, &bumps)?;
}
*config = Config::reset().await?;
let ts = config.get_toolset().await?;
runtime_symlinks::rebuild_for_toolset(config, ts)
.await
.wrap_err("failed to rebuild runtime symlinks")?;
let successful_backends: HashSet<_> = successful_versions
.iter()
.flat_map(|v| {
[
v.ba().short.clone(),
v.ba().tool_name.clone(),
v.ba().full(),
v.ba().full_without_opts(),
]
})
.collect();
let mut upgraded_config_paths: HashSet<_> = outdated
.iter()
.filter(|o| backend_matches(&successful_backends, o.tool_version.ba()))
.filter_map(|o| o.source.path().map(|path| path.to_path_buf()))
.collect();
for tvl in ts.versions.values() {
if backend_matches(&successful_backends, &tvl.backend)
&& let Some(path) = tvl.source.path()
{
upgraded_config_paths.insert(path.to_path_buf());
}
}
for (path, cf) in config.config_files.iter() {
let Ok(trs) = cf.to_tool_request_set() else {
continue;
};
if trs
.tools
.keys()
.any(|ba| backend_matches(&successful_backends, ba))
{
upgraded_config_paths.insert(path.clone());
}
}
let versions_needed_by_tracked = get_versions_needed_by_tracked_configs_excluding_locks(
config,
true,
false,
&upgraded_config_paths,
)
.await?;
for (o, tv) in to_remove {
if successful_versions
.iter()
.any(|v| v.ba() == o.tool_version.ba())
{
let version_key = (
o.tool_version.ba().short.to_string(),
o.tool_version.tv_pathname(),
);
if versions_needed_by_tracked.contains(&version_key) {
debug!(
"Keeping {}@{} because it's still needed by a tracked config",
o.name, tv
);
continue;
}
let pr = mpr.add(&format!("uninstall {}@{}", o.name, tv));
if let Err(e) = self
.uninstall_old_version(config, &o.tool_version, pr.as_ref())
.await
{
warn!("Failed to uninstall old version of {}: {}", o.name, e);
}
}
}
mpr.finish_progress();
for tv in &mut successful_versions {
if matches!(tv.request.source(), ToolSource::Argument)
&& let Some(tvl) = ts.versions.get(tv.ba())
&& matches!(&tvl.source, ToolSource::MiseToml(_))
{
if let Some(config_tv) = tvl.versions.first() {
tv.request = config_tv.request.clone();
} else {
tv.request.set_source(tvl.source.clone());
}
}
}
config::rebuild_shims_and_runtime_symlinks(
config,
ts,
&successful_versions,
crate::lockfile::LockfileUpdateMode::AllowLocked,
)
.await?;
if successful_versions.iter().any(|v| v.short() == "python") {
PIPXBackend::reinstall_all(config)
.await
.unwrap_or_else(|err| {
warn!("failed to reinstall pipx tools: {err}");
});
}
mpr.finish_progress();
Self::print_summary(&outdated, &successful_versions)?;
install_error
}
async fn uninstall_old_version(
&self,
config: &Arc<Config>,
tv: &ToolVersion,
pr: &dyn SingleReport,
) -> Result<()> {
tv.backend()?
.uninstall_version(config, tv, pr, self.dry_run)
.await
.wrap_err_with(|| format!("failed to uninstall {tv}"))?;
pr.finish();
Ok(())
}
fn print_summary(outdated: &[OutdatedInfo], successful_versions: &[ToolVersion]) -> Result<()> {
let upgraded: Vec<_> = outdated
.iter()
.filter(|o| {
successful_versions
.iter()
.any(|v| v.ba() == o.tool_version.ba() && v.version == o.latest)
})
.collect();
if !upgraded.is_empty() {
let s = if upgraded.len() == 1 { "" } else { "s" };
miseprintln!("\nUpgraded {} tool{}:", upgraded.len(), s);
for o in &upgraded {
let from = o.current.as_deref().unwrap_or("(none)");
miseprintln!(" {} {} → {}", o.name, from, o.latest);
}
}
Ok(())
}
fn get_interactive_tool_set(&self, outdated: &Vec<OutdatedInfo>) -> Result<Vec<OutdatedInfo>> {
ui::ctrlc::show_cursor_after_ctrl_c();
let theme = crate::ui::theme::get_theme();
let mut ms = demand::MultiSelect::new("mise upgrade")
.description("Select tools to upgrade")
.filterable(true)
.theme(&theme);
for out in outdated {
ms = ms.option(DemandOption::new(out.clone()));
}
match ms.run() {
Ok(selected) => Ok(selected.into_iter().collect()),
Err(e) => {
Term::stderr().show_cursor()?;
Err(eyre!(e))
}
}
}
fn get_before_date(&self) -> Result<Option<Timestamp>> {
if let Some(minimum_release_age) = &self.minimum_release_age {
return Ok(Some(parse_into_timestamp(minimum_release_age)?));
}
Ok(None)
}
}
fn backend_matches(backends: &HashSet<String>, ba: &BackendArg) -> bool {
backends.contains(&ba.short)
|| backends.contains(&ba.tool_name)
|| backends.contains(&ba.full())
|| backends.contains(&ba.full_without_opts())
}
static AFTER_LONG_HELP: &str = color_print::cstr!(
r#"<bold><underline>Examples:</underline></bold>
# Upgrades node to the latest version matching the range in mise.toml
$ <bold>mise upgrade node</bold>
# Upgrades node to the latest version and bumps the version in mise.toml
$ <bold>mise upgrade node --bump</bold>
# Upgrades all tools to the latest versions
$ <bold>mise upgrade</bold>
# Upgrades all tools to the latest versions and bumps the version in mise.toml
$ <bold>mise upgrade --bump</bold>
# Just print what would be done, don't actually do it
$ <bold>mise upgrade --dry-run</bold>
# Upgrades node and python to the latest versions
$ <bold>mise upgrade node python</bold>
# Upgrade all tools except go
$ <bold>mise upgrade --exclude go</bold>
# Show a multiselect menu to choose which tools to upgrade
$ <bold>mise upgrade --interactive</bold>
# Only upgrade tools defined in local mise.toml, not global ones
$ <bold>mise upgrade --local</bold>
"#
);