use std::collections::BTreeSet;
use std::path::{Component, Path, PathBuf};
use std::sync::{LazyLock, Mutex};
use tracing::trace;
use uv_fs::write_atomic_sync;
use crate::Error;
use crate::wheel::read_record_file;
pub fn uninstall_wheel(dist_info: &Path) -> Result<Uninstall, Error> {
let Some(site_packages) = dist_info.parent() else {
return Err(Error::BrokenVenv(
"dist-info directory is not in a site-packages directory".to_string(),
));
};
let record = {
let record_path = dist_info.join("RECORD");
let mut record_file = match fs_err::File::open(&record_path) {
Ok(record_file) => record_file,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
return Err(Error::MissingRecord(record_path));
}
Err(err) => return Err(err.into()),
};
read_record_file(&mut record_file)?
};
let mut file_count = 0usize;
let mut dir_count = 0usize;
#[cfg(windows)]
let itself = std::env::current_exe().ok();
let mut visited = BTreeSet::new();
for entry in &record {
let path = site_packages.join(&entry.path);
#[cfg(windows)]
if let Some(itself) = itself.as_ref() {
if itself
.file_name()
.is_some_and(|itself| path.file_name().is_some_and(|path| itself == path))
{
if same_file::is_same_file(itself, &path).unwrap_or(false) {
tracing::debug!("Detected self-delete of executable: {}", path.display());
match self_replace::self_delete_outside_path(site_packages) {
Ok(()) => {
trace!("Removed file: {}", path.display());
file_count += 1;
if let Some(parent) = path.parent() {
visited.insert(normalize_path(parent));
}
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
Err(err) => return Err(err.into()),
}
continue;
}
}
}
match fs_err::remove_file(&path) {
Ok(()) => {
trace!("Removed file: {}", path.display());
file_count += 1;
if let Some(parent) = path.parent() {
visited.insert(normalize_path(parent));
}
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
Err(err) => match fs_err::remove_dir_all(&path) {
Ok(()) => {
trace!("Removed directory: {}", path.display());
dir_count += 1;
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
Err(_) => return Err(err.into()),
},
}
}
for path in visited.iter().rev() {
if !path.starts_with(site_packages) {
continue;
}
let mut path = path.as_path();
loop {
if path == site_packages {
break;
}
let pycache = path.join("__pycache__");
match fs_err::remove_dir_all(&pycache) {
Ok(()) => {
trace!("Removed directory: {}", pycache.display());
dir_count += 1;
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
Err(err) => return Err(err.into()),
}
let mut read_dir = match fs_err::read_dir(path) {
Ok(read_dir) => read_dir,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => break,
Err(err) => return Err(err.into()),
};
if read_dir.next().is_some() {
break;
}
fs_err::remove_dir(path)?;
trace!("Removed directory: {}", path.display());
dir_count += 1;
if let Some(parent) = path.parent() {
path = parent;
} else {
break;
}
}
}
Ok(Uninstall {
file_count,
dir_count,
})
}
pub fn uninstall_egg(egg_info: &Path) -> Result<Uninstall, Error> {
let mut file_count = 0usize;
let mut dir_count = 0usize;
let dist_location = egg_info
.parent()
.expect("egg-info directory is not in a site-packages directory");
let namespace_packages = {
let namespace_packages_path = egg_info.join("namespace_packages.txt");
match fs_err::read_to_string(namespace_packages_path) {
Ok(namespace_packages) => namespace_packages
.lines()
.map(ToString::to_string)
.collect::<Vec<_>>(),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
vec![]
}
Err(err) => return Err(err.into()),
}
};
let top_level = {
let top_level_path = egg_info.join("top_level.txt");
match fs_err::read_to_string(&top_level_path) {
Ok(top_level) => top_level
.lines()
.map(ToString::to_string)
.filter(|line| !namespace_packages.contains(line))
.collect::<Vec<_>>(),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
return Err(Error::MissingTopLevel(top_level_path));
}
Err(err) => return Err(err.into()),
}
};
for entry in top_level {
let path = dist_location.join(&entry);
match fs_err::remove_dir_all(&path) {
Ok(()) => {
trace!("Removed directory: {}", path.display());
dir_count += 1;
continue;
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
Err(err) => return Err(err.into()),
}
for extension in &["py", "pyc", "pyo"] {
let path = path.with_extension(extension);
match fs_err::remove_file(&path) {
Ok(()) => {
trace!("Removed file: {}", path.display());
file_count += 1;
break;
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
Err(err) => return Err(err.into()),
}
}
}
match fs_err::remove_dir_all(egg_info) {
Ok(()) => {
trace!("Removed directory: {}", egg_info.display());
dir_count += 1;
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
Err(err) => {
return Err(err.into());
}
}
Ok(Uninstall {
file_count,
dir_count,
})
}
fn normcase(s: &str) -> String {
if cfg!(windows) {
s.replace('/', "\\").to_lowercase()
} else {
s.to_owned()
}
}
static EASY_INSTALL_PTH: LazyLock<Mutex<i32>> = LazyLock::new(Mutex::default);
pub fn uninstall_legacy_editable(egg_link: &Path) -> Result<Uninstall, Error> {
let mut file_count = 0usize;
let contents = fs_err::read_to_string(egg_link)?;
let target_line = contents
.lines()
.find_map(|line| {
let line = line.trim();
if line.is_empty() { None } else { Some(line) }
})
.ok_or_else(|| Error::InvalidEggLink(egg_link.to_path_buf()))?;
let target_line = normcase(target_line);
match fs_err::remove_file(egg_link) {
Ok(()) => {
trace!("Removed file: {}", egg_link.display());
file_count += 1;
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
Err(err) => return Err(err.into()),
}
let site_package = egg_link.parent().ok_or(Error::BrokenVenv(
"`.egg-link` file is not in a directory".to_string(),
))?;
let easy_install = site_package.join("easy-install.pth");
let _guard = EASY_INSTALL_PTH.lock().unwrap();
let content = fs_err::read_to_string(&easy_install)?;
let mut new_content = String::with_capacity(content.len());
let mut removed = false;
for line in content.lines() {
if !removed && line.trim() == target_line {
removed = true;
} else {
new_content.push_str(line);
new_content.push('\n');
}
}
if removed {
write_atomic_sync(&easy_install, new_content)?;
trace!("Removed line from `easy-install.pth`: {target_line}");
}
Ok(Uninstall {
file_count,
dir_count: 0usize,
})
}
#[derive(Debug, Default)]
pub struct Uninstall {
pub file_count: usize,
pub dir_count: usize,
}
fn normalize_path(path: &Path) -> PathBuf {
let mut components = path.components().peekable();
let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().copied() {
components.next();
PathBuf::from(c.as_os_str())
} else {
PathBuf::new()
};
for component in components {
match component {
Component::Prefix(..) => unreachable!(),
Component::RootDir => {
ret.push(component.as_os_str());
}
Component::CurDir => {}
Component::ParentDir => {
ret.pop();
}
Component::Normal(c) => {
ret.push(c);
}
}
}
ret
}