use std::env;
use std::ffi::OsString;
use std::fs;
use std::path::{Path, PathBuf};
use crate::app::App;
use crate::ui::Ui;
use anyhow::{anyhow, Context, Result};
fn prepend_underscore(path: &Path) -> PathBuf {
let mut dst_file_name = OsString::from("_");
dst_file_name.push(
path.file_name()
.unwrap_or_else(|| panic!("{path:?} should have a file name")),
);
path.with_file_name(dst_file_name)
}
fn path_exists(path: &Path) -> bool {
path.is_symlink() || path.exists()
}
pub fn uninstall_cmd(app: &App, ui: &Ui, package_names: &Vec<String>) -> Result<()> {
for package_name in package_names {
uninstall_package(app, ui, package_name)?;
}
Ok(())
}
pub fn uninstall_package(app: &App, ui: &Ui, package_name: &str) -> Result<()> {
let db = &app.database;
let installed_version = match db.get_package_version(package_name)? {
Some(x) => x,
None => {
return Err(anyhow!("Package {} is not installed", package_name));
}
};
ui.info(&format!(
"Uninstalling {} {}",
&package_name, installed_version
));
let ui = ui.nest();
let current_exe_path = env::current_exe().context("Can't find path to current executable")?;
for file in db.get_package_files(package_name)? {
let path = app.install_dir.join(file);
if !path_exists(&path) {
ui.warn(&format!("Expected {:?} to exist, but it does not", &path));
continue;
}
if cfg!(windows) && path == current_exe_path {
let dst_path = prepend_underscore(&path);
ui.info(&format!("Moving {path:?} to {dst_path:?}"));
fs::rename(&path, &dst_path)
.with_context(|| format!("Failed to move {path:?} to {dst_path:?}"))?;
} else {
fs::remove_file(&path).with_context(|| format!("Failed to remove {path:?}"))?;
}
}
db.remove_package(package_name)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(unix)]
use std::os::unix::fs::symlink;
use semver::{Version, VersionReq};
use crate::test_file_utils::*;
#[test]
fn uninstall_should_only_remove_the_package_files() {
let dir = assert_fs::TempDir::new().unwrap();
let app = App::new(&dir).unwrap();
let db = &app.database;
db.create().unwrap();
create_tree(&app.install_dir, &["share/man/f1"]);
let package_files = pathbufset_from_strings(&["bin/b2", "share/man/f2"]);
create_tree_from_path_set(&app.install_dir, &package_files);
db.add_package(
"p2",
&Version::new(1, 0, 0),
&VersionReq::STAR,
&package_files,
)
.unwrap();
let result = uninstall_package(&app, &Ui::default(), "p2");
assert!(result.is_ok(), "{:?}", result);
let expected = pathbufset_from_strings(&["share/man/f1"]);
assert_eq!(list_tree(&app.install_dir).unwrap(), expected);
let result = db.get_package_files("p2").unwrap();
assert!(result.is_empty());
}
#[test]
#[cfg(unix)]
fn uninstall_should_uninstall_broken_symbolic_links() {
let dir = assert_fs::TempDir::new().unwrap();
let app = App::new(&dir).unwrap();
let db = &app.database;
db.create().unwrap();
let bin_dir = app.install_dir.join("bin");
let symbolic_link = bin_dir.join("foo");
fs::create_dir_all(&bin_dir).unwrap();
symlink(bin_dir.join("foo-real"), &symbolic_link).unwrap();
assert!(symbolic_link.is_symlink());
db.add_package(
"foo",
&Version::new(1, 0, 0),
&VersionReq::STAR,
&pathbufset_from_strings(&["bin/foo"]),
)
.unwrap();
let result = uninstall_package(&app, &Ui::default(), "foo");
assert!(result.is_ok(), "{:?}", result);
assert!(!symbolic_link.is_symlink());
}
#[test]
fn test_path_exists() {
let dir = assert_fs::TempDir::new().unwrap();
let existing_file = dir.join("existing");
let non_existing_file = dir.join("non_existing");
fs::write(&existing_file, "").unwrap();
assert!(path_exists(&existing_file));
assert!(!path_exists(&non_existing_file));
#[cfg(unix)]
{
let valid_symlink = dir.join("real_symlink");
let broken_symlink = dir.join("non_existing_symlink");
symlink(&existing_file, &valid_symlink).unwrap();
symlink(&non_existing_file, &broken_symlink).unwrap();
assert!(path_exists(&valid_symlink));
assert!(path_exists(&broken_symlink));
}
}
}