Add support for wheel uninstalls (#77)

Closes #36.
This commit is contained in:
Charlie Marsh 2023-10-09 14:14:33 -04:00 committed by GitHub
parent 239b5893d8
commit b90140e1bc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 256 additions and 27 deletions

View file

@ -1,17 +1,16 @@
//! Takes a wheel and installs it into a venv..
use std::io;
use std::io::{Read, Seek};
use platform_info::PlatformInfoError;
use thiserror::Error;
use zip::result::ZipError;
use zip::ZipArchive;
pub use install_location::{normalize_name, InstallLocation, LockedDir};
use platform_host::{Arch, Os};
pub use record::RecordEntry;
pub use script::Script;
pub use uninstall::uninstall_wheel;
pub use wheel::{
get_script_launcher, install_wheel, parse_key_value_file, read_record_file, relative_to,
SHEBANG_PYTHON,
@ -24,6 +23,7 @@ mod record;
#[cfg(any(target_os = "macos", target_os = "ios"))]
mod reflink;
mod script;
mod uninstall;
pub mod unpacked;
mod wheel;
@ -74,13 +74,3 @@ impl Error {
}
}
}
pub fn do_thing(reader: impl Read + Seek) -> Result<(), Error> {
let x = tempfile::tempdir()?;
let mut archive =
ZipArchive::new(reader).map_err(|err| Error::from_zip_error("(index)".to_string(), err))?;
archive.extract(x.path()).unwrap();
Ok(())
}

View file

@ -0,0 +1,129 @@
use std::collections::BTreeSet;
use std::path::{Component, Path, PathBuf};
use fs_err as fs;
use fs_err::File;
use tracing::debug;
use crate::{read_record_file, Error};
/// Uninstall the wheel represented by the given `dist_info` directory.
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(),
));
};
// Read the RECORD file.
let mut record_file = File::open(dist_info.join("RECORD"))?;
let record = read_record_file(&mut record_file)?;
let mut file_count = 0usize;
let mut dir_count = 0usize;
// Uninstall the files, keeping track of any directories that are left empty.
let mut visited = BTreeSet::new();
for entry in &record {
let path = site_packages.join(&entry.path);
match fs::remove_file(&path) {
Ok(()) => {
debug!("Removed file: {}", path.display());
file_count += 1;
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
Err(err) => return Err(err.into()),
}
if let Some(parent) = path.parent() {
visited.insert(normalize_path(parent));
}
}
// If any directories were left empty, remove them. Iterate in reverse order such that we visit
// the deepest directories first.
for path in visited.iter().rev() {
// No need to look at directories outside of `site-packages` (like `bin`).
if !path.starts_with(site_packages) {
continue;
}
// Iterate up the directory tree, removing any empty directories. It's insufficient to
// rely on `visited` alone here, because we may end up removing a directory whose parent
// directory doesn't contain any files, leaving the _parent_ directory empty.
let mut path = path.as_path();
loop {
// If we reach the site-packages directory, we're done.
if path == site_packages {
break;
}
// Try to read from the directory. If it doesn't exist, assume we deleted it in a
// previous iteration.
let mut read_dir = match fs::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 the directory is not empty, we're done.
if read_dir.next().is_some() {
break;
}
fs::remove_dir(path)?;
debug!("Removed directory: {}", path.display());
dir_count += 1;
if let Some(parent) = path.parent() {
path = parent;
} else {
break;
}
}
}
Ok(Uninstall {
file_count,
dir_count,
})
}
#[derive(Debug)]
pub struct Uninstall {
/// The number of files that were removed during the uninstallation.
pub file_count: usize,
/// The number of directories that were removed during the uninstallation.
pub dir_count: usize,
}
/// Normalize a path, removing things like `.` and `..`.
///
/// Source: <https://github.com/rust-lang/cargo/blob/b48c41aedbd69ee3990d62a0e2006edbb506a480/crates/cargo-util/src/paths.rs#L76C1-L109C2>
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
}