use std::{
ffi::OsStr,
fmt::Display,
io::{Read, Write},
path::PathBuf,
};
use anyhow::{Context, bail};
use thiserror::Error;
use crate::{
channel::{Channel, Component, InstalledFile},
config::Config,
manifest::Manifest,
version::Authority,
};
#[derive(Error, Debug)]
pub enum UninstallError {
#[error("Could not find installation-successful or .installation-in-progress at {0}")]
MissingInstalledComponentsFile(PathBuf),
#[error("Could not find channel.json file at: {0}. {1}")]
ChannelJsonMissing(PathBuf, String),
#[error("Ill-formed channel.json at: {0}. Contents: {1}. {2}")]
IllFormedChannelJson(PathBuf, String, String),
#[error("Couldn't delete file at: {0}. {1}")]
FailedToDeleteFile(PathBuf, String),
#[error("Failed to uninstall package: {0}, with status: {1}. {2}")]
FailedToUninstallPackage(String, i32, String),
#[error("Internal cargo error: {0}")]
InternalCargoError(String),
}
pub fn uninstall(
config: &Config,
channel: &Channel,
local_manifest: &mut Manifest,
) -> anyhow::Result<()> {
let installed_toolchains_dir = config.midenup_home.join("toolchains");
let toolchain_dir = installed_toolchains_dir.join(format!("{}", &channel.name));
if !toolchain_dir.exists() {
bail!("Channel {} is not installed, nothing to uninstall.", channel.name);
};
match uninstall_channel(&toolchain_dir) {
Ok(()) => (),
Err(UninstallError::MissingInstalledComponentsFile(path)) => {
println!(
"WARNING: Could not find installation-successful or .installation-in-progress at \
{}.
Uninstallation will procede by deleting toolchain manually, instead of going through cargo.\n",
path.display()
)
},
Err(err) => bail!("Failed to uninstall {err}"),
}
{
let stable_symlink = installed_toolchains_dir.join("stable");
let symlink_points_to_this_channel = std::fs::read_link(&stable_symlink)
.ok()
.map(|target| target == toolchain_dir)
.unwrap_or(false);
if symlink_points_to_this_channel {
std::fs::remove_file(stable_symlink).context("Couldn't remove symlink")?;
}
}
local_manifest.remove_channel(channel.name.clone());
let local_manifest_path = config.midenup_home.join("manifest").with_extension("json");
let mut local_manifest_file =
std::fs::File::create(&local_manifest_path).with_context(|| {
format!(
"failed to create file for install script at '{}'",
local_manifest_path.display()
)
})?;
local_manifest_file
.write_all(
serde_json::to_string_pretty(&local_manifest)
.context("Couldn't serialize local manifest")?
.as_bytes(),
)
.context("Couldn't create local manifest file")?;
std::fs::remove_dir_all(&toolchain_dir).context(format!(
"midenup failed to delete the toolchain directory.
However, manual removal should be safe. The toolchain's PATH is the following:
{}
",
toolchain_dir.display()
))?;
Ok(())
}
fn uninstall_channel(toolchain_dir: &PathBuf) -> Result<(), UninstallError> {
let installed_components_path = {
let installed_successfully = toolchain_dir.join("installation-successful");
let installation_in_progress = toolchain_dir.join(".installation-in-progress");
if installed_successfully.exists() {
installed_successfully
} else if installation_in_progress.exists() {
installation_in_progress
} else {
return Err(UninstallError::MissingInstalledComponentsFile(
toolchain_dir.to_path_buf(),
));
}
};
let channel_content_path = toolchain_dir.join(".installed_channel.json");
let channel_content = std::fs::read_to_string(&channel_content_path).map_err(|err| {
UninstallError::ChannelJsonMissing(channel_content_path.clone(), err.to_string())
})?;
let channel = serde_json::from_str::<Channel>(&channel_content).map_err(|err| {
UninstallError::IllFormedChannelJson(channel_content_path, channel_content, err.to_string())
})?;
let components: Vec<&Component> = std::fs::read_to_string(&installed_components_path)
.unwrap()
.lines()
.map(String::from)
.map(|channel_name| channel.get_component(channel_name))
.collect::<Option<Vec<&Component>>>()
.expect("Couldn't find installed component in channel");
let _ = std::fs::remove_file(installed_components_path);
let (installed_libraries, installed_executables): (Vec<&Component>, Vec<&Component>) =
components
.iter()
.partition(|c| matches!(c.get_installed_file(), InstalledFile::Library { .. }));
for lib in installed_libraries {
let lib_path = toolchain_dir.join("lib").join(lib.name.as_ref()).with_extension("masp");
std::fs::remove_file(&lib_path)
.map_err(|err| UninstallError::FailedToDeleteFile(lib_path, err.to_string()))?;
}
for exe in installed_executables {
match &exe.version {
Authority::Cargo { package, .. } => {
let package_name = package.as_deref().unwrap_or(exe.name.as_ref());
uninstall_executable(package_name, toolchain_dir)?;
},
Authority::Git { crate_name, .. } => {
uninstall_executable(crate_name, toolchain_dir)?;
},
Authority::Path { crate_name, .. } => {
uninstall_executable(crate_name, toolchain_dir)?;
},
}
}
Ok(())
}
pub fn uninstall_executable(
name: impl AsRef<OsStr> + Display,
root_dir: impl AsRef<OsStr>,
) -> Result<(), UninstallError> {
let mut remove_exe = std::process::Command::new("cargo")
.arg("uninstall")
.arg(&name)
.arg("--root")
.arg(&root_dir)
.stderr(std::process::Stdio::inherit())
.stdout(std::process::Stdio::inherit())
.spawn()
.map_err(|err| UninstallError::InternalCargoError(err.to_string()))?;
let status = remove_exe
.wait()
.map_err(|err| UninstallError::InternalCargoError(err.to_string()))?;
if !status.success() {
let error = remove_exe.stderr.take();
let error_msg = if let Some(mut error) = error {
let mut stderr_msg = String::new();
let read_err_msg = error.read_to_string(&mut stderr_msg);
if read_err_msg.is_err() {
String::from("")
} else {
format!("The following error was raised: {stderr_msg}")
}
} else {
String::from("")
};
return Err(UninstallError::FailedToUninstallPackage(
name.to_string(),
status.code().unwrap_or(1),
error_msg,
));
}
Ok(())
}