use std::{
collections::HashSet,
hash::{Hash, Hasher},
};
use anyhow::Context;
use colored::Colorize;
use crate::{
channel::{
Channel, Component, InstalledFile, MigrationStrategy, UpstreamChannel, UpstreamMatch,
UserChannel,
},
commands::{self},
config::Config,
manifest::Manifest,
options::{InstallationOptions, PathUpdate, UpdateOptions},
profile::Profile,
version::Authority,
};
pub fn update(
config: &Config,
channel_type: Option<&UserChannel>,
local_manifest: &mut Manifest,
options: &UpdateOptions,
) -> anyhow::Result<()> {
let last_updated = local_manifest.last_updated();
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
",
)?;
println!(
"syncing channel updates for stable (last update was {last_updated} as {})",
&local_stable.name
);
let upstream_stable = config
.manifest
.get_latest_stable()
.context("ERROR: No stable channel found in upstream")?;
println!(
"latest stable is version {} (upstream last updated on {})",
&upstream_stable.name,
config.manifest.last_updated()
);
if upstream_stable.name > local_stable.name {
let component_subset: Option<HashSet<_>> = if local_stable.is_partially_installed()
{
Some(local_stable.components.iter().map(|comp| comp.name.clone()).collect())
} else {
None
};
let channel_to_install = {
let components = upstream_stable
.components
.clone()
.into_iter()
.filter(|comp| {
if let Some(component_subset) = &component_subset {
let name = &comp.name;
component_subset.contains(name)
} else {
true
}
})
.collect();
Channel {
name: upstream_stable.name.clone(),
alias: upstream_stable.alias.clone(),
tags: local_stable.tags.clone(),
components,
}
};
let install_options = InstallationOptions::from(*options);
commands::install(config, &channel_to_install, local_manifest, &install_options)?
} 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();
println!(
"syncing channel updates for {} (last update was {last_updated})",
&local_channel.name
);
let upstream_counterpart =
local_channel.find_upstream_counterpart(config).context(format!(
"ERROR: Couldn't find a channel upstream with version {version}. Maybe it got \
removed."
))?;
println!("upstream last updated on {}", config.manifest.last_updated());
update_channel(config, &local_channel, &upstream_counterpart, local_manifest, options)?
},
None => {
let mut channels_to_update = Vec::new();
for local_channel in local_manifest.get_channels() {
let upstream_counterpart = local_channel.find_upstream_counterpart(config);
let Some(upstream_channel) = upstream_counterpart else {
continue;
};
channels_to_update.push((local_channel.clone(), upstream_channel.clone()));
}
for (local_channel, upstream_channel) in channels_to_update {
println!(
"syncing channel updates for {} (last update was {last_updated})",
&local_channel.name
);
println!("upstream last updated on {}", config.manifest.last_updated());
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: &UpstreamChannel,
local_manifest: &mut Manifest,
options: &UpdateOptions,
) -> anyhow::Result<()> {
let update = match compute_update(local_channel, upstream_channel, options)? {
UpdatePlan::Abort => {
println!("Aborting update of {} due to user input/configuration", local_channel);
return Ok(());
},
UpdatePlan::Skip => {
println!("Toolchain {} is up to date", local_channel);
return Ok(());
},
UpdatePlan::Pending(update) => update,
};
display_warnings(&update, options);
println!("Updating toolchain {}..", &local_channel.name);
let Update {
channel_to_install,
components_to_uninstall,
channel_to_uninstall,
} = update;
let install_options = InstallationOptions {
profile: Profile::Minimal,
verbose: options.verbose,
components_to_uninstall,
};
commands::install(config, &channel_to_install, local_manifest, &install_options)?;
if let Some(channel_to_install) = channel_to_uninstall {
commands::uninstall(config, &channel_to_install, local_manifest)?;
};
Ok(())
}
enum InteractiveResult {
Cancel,
UpdateComponent,
DontUpdateComponent,
}
fn handle_path_uninstall_interactive(component: &Component) -> anyhow::Result<InteractiveResult> {
let component_name = &component.name;
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 {component_name}");
Ok(InteractiveResult::UpdateComponent)
},
"c" => {
println!("Cancelling update, no changes will be applied.");
Ok(InteractiveResult::Cancel)
},
_ => {
println!("Skipping {component_name}, it will not be updated");
Ok(InteractiveResult::DontUpdateComponent)
},
}
}
#[derive(Debug, Clone)]
pub enum UpdateStatus {
Added,
Removed,
NeedsUpdate,
Migrated { strategy: MigrationStrategy },
UpToDate,
}
#[derive(Debug, Clone)]
struct ComponentByName<'a>(&'a Component);
impl PartialEq for ComponentByName<'_> {
fn eq(&self, other: &Self) -> bool {
self.0.name == other.0.name
}
}
impl Eq for ComponentByName<'_> {}
impl Hash for ComponentByName<'_> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.name.hash(state);
}
}
#[derive(Debug, Clone)]
pub struct ComponentUpdate {
pub component: Component,
pub motive: UpdateStatus,
}
impl ComponentUpdate {
fn new(component: Component, motive: UpdateStatus) -> ComponentUpdate {
ComponentUpdate { component, motive }
}
}
#[allow(clippy::large_enum_variant)]
enum UpdatePlan {
Abort,
Skip,
Pending(Update),
}
#[derive(Debug, Clone)]
pub struct Update {
pub channel_to_install: Channel,
pub components_to_uninstall: Vec<Component>,
pub channel_to_uninstall: Option<Channel>,
}
impl Update {
fn new(
channel_to_install: Channel,
components_to_uninstall: Vec<Component>,
channel_to_uninstall: Option<Channel>,
) -> Update {
Update {
channel_to_install,
components_to_uninstall,
channel_to_uninstall,
}
}
}
fn compute_update(
older: &Channel,
newer: &UpstreamChannel,
options: &UpdateOptions,
) -> anyhow::Result<UpdatePlan> {
struct MigrationEffects<'a> {
strategy: Option<&'a MigrationStrategy>,
newer: &'a Channel,
older: &'a Channel,
}
impl<'a> MigrationEffects<'a> {
fn new(upstream_channel: &'a UpstreamChannel, older: &'a Channel) -> Self {
match &upstream_channel.upstream_match {
UpstreamMatch::UpstreamCounterpart => Self {
strategy: None,
newer: &upstream_channel.channel,
older,
},
UpstreamMatch::Migrated(migration_strategy) => {
match migration_strategy {
MigrationStrategy::NameChange { old_channel: _old_channel } => {
if older.name == upstream_channel.channel.name {
Self {
strategy: None,
newer: &upstream_channel.channel,
older,
}
} else {
Self {
strategy: Some(migration_strategy),
newer: &upstream_channel.channel,
older,
}
}
},
}
},
}
}
fn migrate_channel(&self, channel: &mut Channel) {
match self.strategy {
Some(MigrationStrategy::NameChange { .. }) => {
channel.name = self.newer.name.clone();
},
None => (),
}
}
fn channel_to_uninstall(&self) -> Option<Channel> {
#[allow(clippy::manual_map)]
match self.strategy {
Some(MigrationStrategy::NameChange { .. }) => Some(self.older.clone()),
None => None,
}
}
fn required(&self) -> bool {
self.strategy.is_some()
}
}
let new_channel: HashSet<ComponentByName> =
newer.channel.components.iter().map(ComponentByName).collect();
let current: HashSet<ComponentByName> = older.components.iter().map(ComponentByName).collect();
let new_components = new_channel
.difference(¤t)
.filter(|_| !older.is_partially_installed())
.map(|&ComponentByName(comp)| ComponentUpdate::new(comp.clone(), UpdateStatus::Added));
let old_components = current
.difference(&new_channel)
.map(|&ComponentByName(comp)| ComponentUpdate::new(comp.clone(), UpdateStatus::Removed));
let changed_components = current.intersection(&new_channel);
let mut components_to_install = Vec::from_iter(new_components);
let mut components_to_uninstall = Vec::from_iter(old_components.map(|cu| cu.component));
for component_by_name in changed_components {
let new_component = new_channel.get(component_by_name).unwrap().0;
let current_component = current.get(component_by_name).unwrap().0;
let update_status = {
if let UpstreamMatch::Migrated(strategy) = &newer.upstream_match {
UpdateStatus::Migrated { strategy: strategy.clone() }
} else if !current_component.is_up_to_date(new_component) {
UpdateStatus::NeedsUpdate
} else {
UpdateStatus::UpToDate
}
};
if matches!(update_status, UpdateStatus::NeedsUpdate) {
match should_skip_component_update(current_component, options, older)? {
ComponentUpdateDecision::Abort => return Ok(UpdatePlan::Abort),
ComponentUpdateDecision::Keep(preserved_component) => {
components_to_install
.push(ComponentUpdate::new(preserved_component, update_status));
},
ComponentUpdateDecision::Update => {
components_to_uninstall.push(current_component.clone());
components_to_install
.push(ComponentUpdate::new(new_component.clone(), update_status));
},
}
} else {
components_to_install.push(ComponentUpdate::new(new_component.clone(), update_status));
}
}
let migration = MigrationEffects::new(newer, older);
{
let all_components_up_to_date = components_to_install
.iter()
.all(|cu| matches!(cu.motive, UpdateStatus::UpToDate));
if all_components_up_to_date && components_to_uninstall.is_empty() && !migration.required()
{
return Ok(UpdatePlan::Skip);
}
}
let channel_to_install = {
let components_to_install = components_to_install
.into_iter()
.map(|comp_update| comp_update.component)
.collect::<Vec<_>>();
let mut channel_to_install = older.clone();
channel_to_install.components = components_to_install;
migration.migrate_channel(&mut channel_to_install);
channel_to_install
};
let channel_to_uninstall = migration.channel_to_uninstall();
let update = Update::new(channel_to_install, components_to_uninstall, channel_to_uninstall);
Ok(UpdatePlan::Pending(update))
}
#[allow(clippy::large_enum_variant)]
enum ComponentUpdateDecision {
Abort,
Keep(Component),
Update,
}
fn should_skip_component_update(
component: &Component,
options: &UpdateOptions,
local_channel: &Channel,
) -> anyhow::Result<ComponentUpdateDecision> {
let skip_update = match component.get_installed_file() {
InstalledFile::Library { .. } => false,
InstalledFile::Executable { .. } => match component.version {
Authority::Cargo { .. } | Authority::Git { .. } => false,
Authority::Path { .. } => match options.path_update {
PathUpdate::Interactive => match handle_path_uninstall_interactive(component)? {
InteractiveResult::Cancel => return Ok(ComponentUpdateDecision::Abort),
InteractiveResult::UpdateComponent => false,
InteractiveResult::DontUpdateComponent => true,
},
PathUpdate::All => false,
PathUpdate::Off => true,
},
},
};
if skip_update && let Some(old) = local_channel.get_component(&component.name) {
Ok(ComponentUpdateDecision::Keep(old.clone()))
} else {
Ok(ComponentUpdateDecision::Update)
}
}
fn display_warnings(update: &Update, options: &UpdateOptions) {
{
let components_from_path: Vec<String> = update
.channel_to_install
.components
.iter()
.filter_map(|component| match &component.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!(
"\n{}: 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);
}
}
}
{
if let Some(old_channel) = &update.channel_to_uninstall {
let migrated_components: Vec<String> = update
.channel_to_install
.components
.iter()
.map(|component| {
format!(
"- {} from {} into {}",
component.name, old_channel, update.channel_to_install
)
})
.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);
}
}
}
}
}