use crate::backend::pipx::PIPXBackend;
use crate::cli::args::ToolArg;
use crate::config::{config_file, Config};
use crate::file::display_path;
use crate::toolset::outdated_info::OutdatedInfo;
use crate::toolset::{InstallOptions, ResolveOptions, ToolVersion, ToolsetBuilder};
use crate::ui::multi_progress_report::MultiProgressReport;
use crate::ui::progress_report::SingleReport;
use crate::{config, ui};
use console::Term;
use demand::DemandOption;
use eyre::{eyre, Context, Result};
#[derive(Debug, clap::Args)]
#[clap(visible_alias = "up", verbatim_doc_comment, after_long_help = AFTER_LONG_HELP)]
pub struct Upgrade {
#[clap(value_name = "TOOL@VERSION", verbatim_doc_comment)]
tool: Vec<ToolArg>,
#[clap(long, short = 'n', verbatim_doc_comment)]
dry_run: bool,
#[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, overrides_with = "jobs")]
raw: bool,
}
impl Upgrade {
pub fn run(self) -> Result<()> {
let config = Config::try_get()?;
let ts = ToolsetBuilder::new().with_args(&self.tool).build(&config)?;
let mut outdated = ts.list_outdated_versions(self.bump);
if self.interactive && !outdated.is_empty() {
outdated = self.get_interactive_tool_set(&outdated)?;
} else if !self.tool.is_empty() {
outdated.retain(|o| self.tool.iter().any(|t| &t.ba == o.tool_version.ba()));
}
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(&config, outdated)?;
}
Ok(())
}
fn upgrade(&self, config: &Config, outdated: Vec<OutdatedInfo>) -> Result<()> {
let mpr = MultiProgressReport::get();
let mut ts = ToolsetBuilder::new().with_args(&self.tool).build(config)?;
let config_file_updates = outdated
.iter()
.filter_map(|o| {
if let (Some(path), Some(_bump)) = (o.source.path(), &o.bump) {
match config_file::parse(path) {
Ok(cf) => Some((o, cf)),
Err(e) => {
warn!("failed to parse {}: {e}", display_path(path));
None
}
}
} else {
None
}
})
.filter(|(o, cf)| {
if let Ok(trs) = cf.to_tool_request_set() {
if let Some(versions) = trs.tools.get(o.tool_request.ba()) {
if versions.len() != 1 {
warn!("upgrading multiple versions with --bump is not yet supported");
return false;
}
}
}
true
})
.collect::<Vec<_>>();
let to_remove = outdated
.iter()
.filter_map(|o| o.current.as_ref().map(|current| (o, current)))
.collect::<Vec<_>>();
if self.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())
);
}
return Ok(());
}
let opts = InstallOptions {
force: true,
jobs: self.jobs,
raw: self.raw,
resolve_options: ResolveOptions {
use_locked_version: false,
latest_versions: true,
},
..Default::default()
};
let new_versions = outdated.iter().map(|o| o.tool_request.clone()).collect();
let versions = ts.install_all_versions(new_versions, &mpr, &opts)?;
for (o, mut cf) in config_file_updates {
cf.replace_versions(o.tool_request.ba(), vec![o.tool_request.clone()])?;
cf.save()?;
}
for (o, tv) in to_remove {
let pr = mpr.add(&format!("uninstall {}@{}", o.name, tv));
self.uninstall_old_version(&o.tool_version, &pr)?;
}
config::rebuild_shims_and_runtime_symlinks(&versions)?;
if versions.iter().any(|v| v.short() == "python") {
PIPXBackend::reinstall_all().unwrap_or_else(|err| {
warn!("failed to reinstall pipx tools: {err}");
})
}
Ok(())
}
fn uninstall_old_version(&self, tv: &ToolVersion, pr: &Box<dyn SingleReport>) -> Result<()> {
tv.backend()?
.uninstall_version(tv, pr, self.dry_run)
.wrap_err_with(|| format!("failed to uninstall {tv}"))?;
pr.finish();
Ok(())
}
fn get_interactive_tool_set(&self, outdated: &Vec<OutdatedInfo>) -> Result<Vec<OutdatedInfo>> {
ui::ctrlc::show_cursor_after_ctrl_c();
let mut ms = demand::MultiSelect::new("mise upgrade")
.description("Select tools to upgrade")
.filterable(true)
.min(1);
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))
}
}
}
}
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>
# Show a multiselect menu to choose which tools to upgrade
$ <bold>mise upgrade --interactive</bold>
"#
);