use std::collections::HashSet;
use anyhow::{Context, bail};
use colored::Colorize;
use crate::{
channel::{
Channel, Component, InstalledFile, MigrationStrategy, UserChannel, is_toolchain_deleted,
},
commands::{self, uninstall::uninstall_executable},
config::Config,
manifest::Manifest,
options::{PathUpdate, UpdateOptions},
version::Authority,
};
pub fn update(
config: &Config,
channel_type: Option<&UserChannel>,
local_manifest: &mut Manifest,
options: &UpdateOptions,
) -> anyhow::Result<()> {
match channel_type {
Some(UserChannel::Stable) => {
let local_stable = local_manifest.get_latest_stable().context(
"No stable version was found. To install it, try running:
midenup install stable
",
)?;
let upstream_stable = config
.manifest
.get_latest_stable()
.context("ERROR: No stable channel found in upstream")?;
if upstream_stable.name > local_stable.name {
let component_subset = if local_stable.is_partially_installed() {
Some(local_stable.components.clone())
} else {
None
};
let channel_to_install = {
let components = upstream_stable
.components
.iter()
.filter(|comp| {
if let Some(component_subset) = &component_subset {
component_subset.contains(comp)
} else {
true
}
})
.cloned()
.collect();
Channel {
name: upstream_stable.name.clone(),
alias: upstream_stable.alias.clone(),
tags: local_stable.tags.clone(),
components,
}
};
commands::install(
config,
&channel_to_install,
local_manifest,
&((*options).into()),
)?
} else {
println!("Nothing to update, you are all up to date");
}
},
Some(UserChannel::Version(version)) => {
let local_channel = local_manifest
.get_channel(&UserChannel::Version(version.clone()))
.context(format!("ERROR: No installed channel found with version {version}"))?
.clone();
let upstream_channel = config
.manifest
.get_channel(&UserChannel::Version(version.clone()))
.context(format!(
"ERROR: Couldn't find a channel upstream with version {version}. Maybe it got \
removed."
))?;
update_channel(config, &local_channel, upstream_channel, local_manifest, options)?
},
None => {
let mut channels_to_update = Vec::new();
for local_channel in local_manifest.get_channels() {
let upstream_channel =
config.manifest.get_channels().find(|up_c| *up_c == local_channel);
let Some(upstream_channel) = upstream_channel else {
continue;
};
channels_to_update.push((local_channel.clone(), upstream_channel.clone()));
}
for (local_channel, upstream_channel) in channels_to_update {
update_channel(config, &local_channel, &upstream_channel, local_manifest, options)?;
}
},
Some(UserChannel::Nightly) => todo!(),
Some(UserChannel::Other(_)) => todo!(),
}
Ok(())
}
fn update_channel(
config: &Config,
local_channel: &Channel,
upstream_channel: &Channel,
local_manifest: &mut Manifest,
options: &UpdateOptions,
) -> anyhow::Result<()> {
let installed_toolchains_dir = config.midenup_home.join("toolchains");
let toolchain_dir = installed_toolchains_dir.join(format!("{}", &local_channel.name));
let mut channel_to_install = upstream_channel.clone();
let comp_to_delete_with_motive = components_to_update(local_channel, &channel_to_install);
if comp_to_delete_with_motive.is_empty() {
println!("Toolchain {} is up to date", local_channel);
return Ok(());
}
display_warnings(&comp_to_delete_with_motive, upstream_channel, options);
let mut exes_to_uninstall = Vec::new();
let mut libs_to_uninstall = Vec::new();
for (component, motive) in comp_to_delete_with_motive {
if matches!(motive, UpdateMotive::Added) {
continue;
}
match component.get_installed_file() {
InstalledFile::Library { library_name, .. } => {
let lib_path = toolchain_dir.join("lib").join(library_name);
libs_to_uninstall.push(lib_path);
},
InstalledFile::Executable { .. } => {
let executable_to_uninstall: Option<String> = match component.version {
Authority::Cargo { package, .. } => {
let package_name = package.unwrap_or(component.name.to_string());
Some(package_name)
},
Authority::Git { crate_name, .. } => Some(crate_name),
Authority::Path { crate_name, .. } => match options.path_update {
PathUpdate::Interactive => {
match handle_path_uninstall_interactive(crate_name)? {
InteractiveResult::Cancel => return Ok(()),
InteractiveResult::ComponentUpdate(update_decision) => {
update_decision
},
}
},
PathUpdate::All => Some(crate_name),
PathUpdate::Off => None,
},
};
if let Some(executable_name) = executable_to_uninstall {
exes_to_uninstall.push(executable_name);
} else {
let Some(component_to_install) =
channel_to_install.get_component_mut(&component.name)
else {
continue;
};
let local_component =
local_channel.get_component(&component_to_install.name).unwrap();
*component_to_install = local_component.clone();
}
},
}
}
{
let installation_indicator = toolchain_dir.join("installation-successful");
match std::fs::remove_file(&installation_indicator) {
Ok(()) => (),
Err(e) if matches!(e.kind(), std::io::ErrorKind::NotFound) => (),
Err(e) => bail!(format!(
"Couldn't delete installation complete indicator in: {}because of {e}",
&installation_indicator.display()
)),
}
for lib in libs_to_uninstall {
std::fs::remove_file(&lib).context(format!("Couldn't delete {}", lib.display()))?;
}
for exe in exes_to_uninstall {
let result = uninstall_executable(exe, &toolchain_dir);
match result {
Err(commands::uninstall::UninstallError::FailedToUninstallPackage(name, ..)) => {
println!(
"INFO: Failed to uninstall old version of {name}. Proceeding regardless."
);
continue;
},
Err(err) => return Err(err)?,
Ok(_) => continue,
};
}
commands::install(config, &channel_to_install, local_manifest, &((*options).into()))?;
let is_entirely_removed = is_toolchain_deleted(&toolchain_dir);
if is_entirely_removed {
commands::uninstall(config, local_channel, local_manifest)?;
};
}
Ok(())
}
enum InteractiveResult {
Cancel,
ComponentUpdate(Option<String>),
}
fn handle_path_uninstall_interactive(crate_name: String) -> anyhow::Result<InteractiveResult> {
println!(
"Would you like to update this component? (N/y/c)
- N: no, skip this component
- y: yes, update this component
- c: cancel the update all-together (no changes will be applied)"
);
let mut input = String::new();
std::io::stdin().read_line(&mut input).context("Failed to read input")?;
let input = input.trim().to_ascii_lowercase();
match input.as_str() {
"y" => {
println!("Updating {crate_name}");
Ok(InteractiveResult::ComponentUpdate(Some(crate_name)))
},
"c" => {
println!("Cancelling update, no changes will be applied.");
Ok(InteractiveResult::Cancel)
},
_ => {
println!("Skipping {crate_name}, it will not be updated");
Ok(InteractiveResult::ComponentUpdate(None))
},
}
}
#[derive(Debug, Clone)]
pub enum UpdateMotive {
Added,
Removed,
NewerVersion,
Migrated { strategy: MigrationStrategy },
}
pub fn components_to_update(older: &Channel, newer: &Channel) -> Vec<(Component, UpdateMotive)> {
let new_channel: HashSet<&Component> = HashSet::from_iter(newer.components.iter());
let current = HashSet::from_iter(older.components.iter());
let new_components = new_channel.difference(¤t).map(|comp| (comp, UpdateMotive::Added));
let old_components = current.difference(&new_channel).map(|comp| (comp, UpdateMotive::Removed));
let components_to_update = current
.iter()
.filter(|comp| new_channel.contains(**comp))
.filter_map(|current_component| {
let new_component = new_channel.get(*current_component);
match new_component {
None => Some((current_component, UpdateMotive::Added)),
Some(new_component) => {
if let Some(strategy) = newer.migrated_into() {
Some((
current_component,
UpdateMotive::Migrated { strategy: strategy.clone() },
))
} else if !current_component.is_up_to_date(new_component) {
Some((current_component, UpdateMotive::NewerVersion))
} else {
None
}
},
}
});
let components = new_components
.chain(old_components)
.chain(components_to_update)
.map(|(comp, motive)| ((*comp).clone(), motive));
Vec::from_iter(components)
}
fn display_warnings(
components_with_motive: &[(Component, UpdateMotive)],
newer: &Channel,
options: &UpdateOptions,
) {
let components_with_motive = components_with_motive.iter();
{
let components_from_path: Vec<String> = components_with_motive
.clone()
.filter_map(|(comp, _)| match &comp.version {
Authority::Path { path, crate_name, .. } => Some((path, crate_name)),
_ => None,
})
.map(|(path, crate_name)| {
format!("- {} is installed from {}.\n", crate_name.bold(), path.display(),)
})
.collect();
if !components_from_path.is_empty() {
println!(
"{}: The following elements are installed from a specific path in the filesystem.",
"WARNING".yellow().bold(),
);
if matches!(options.path_update, PathUpdate::Off) {
println!(
"
To make midenup update them all, pass the '--path-update=all' flag to `midenup update`.
Alternatively, pass the '--path-update=interactive' flag to interactively select which \
path-managed components to update.",
);
}
for component_message in components_from_path {
println!("{}", component_message);
}
}
}
{
let migrated_components: Vec<String> = components_with_motive
.filter_map(|(component, motive)| match &motive {
UpdateMotive::Migrated { strategy } => Some((component, strategy)),
_ => None,
})
.map(|(component, strategy)| match strategy {
MigrationStrategy::NameChange { old_channel } => {
format!("- {} from {} into {}", component.name, old_channel, newer)
},
})
.collect();
if !migrated_components.is_empty() {
println!(
"{}: The following elements are going to be migrated.",
"WARNING".yellow().bold(),
);
for component_message in migrated_components {
println!("{}", component_message);
}
}
}
}