-
Notifications
You must be signed in to change notification settings - Fork 978
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for wheel uninstalls (#77)
Closes #36.
- Loading branch information
1 parent
239b589
commit b90140e
Showing
11 changed files
with
256 additions
and
27 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
use std::path::Path; | ||
|
||
use anyhow::Result; | ||
use tracing::debug; | ||
|
||
use platform_host::Platform; | ||
use puffin_interpreter::PythonExecutable; | ||
|
||
use crate::commands::ExitStatus; | ||
|
||
/// Uninstall a package from the current environment. | ||
pub(crate) async fn uninstall(name: &str, cache: Option<&Path>) -> Result<ExitStatus> { | ||
// Detect the current Python interpreter. | ||
let platform = Platform::current()?; | ||
let python = PythonExecutable::from_env(platform, cache)?; | ||
debug!( | ||
"Using Python interpreter: {}", | ||
python.executable().display() | ||
); | ||
|
||
// Uninstall the package from the current environment. | ||
puffin_installer::uninstall(name, &python).await?; | ||
|
||
Ok(ExitStatus::Success) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,11 @@ | ||
pub use distribution::{Distribution, LocalDistribution, RemoteDistribution}; | ||
pub use index::LocalIndex; | ||
pub use install::install; | ||
pub use uninstall::uninstall; | ||
|
||
mod cache; | ||
mod distribution; | ||
mod index; | ||
mod install; | ||
mod uninstall; | ||
mod vendor; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
use anyhow::{anyhow, Result}; | ||
use tracing::info; | ||
|
||
use puffin_interpreter::PythonExecutable; | ||
use puffin_package::package_name::PackageName; | ||
|
||
/// Uninstall a package from the specified Python environment. | ||
pub async fn uninstall(name: &str, python: &PythonExecutable) -> Result<()> { | ||
// Index the current `site-packages` directory. | ||
let site_packages = puffin_interpreter::SitePackages::from_executable(python).await?; | ||
|
||
// Locate the package in the environment. | ||
let Some(dist_info) = site_packages.get(&PackageName::normalize(name)) else { | ||
return Err(anyhow!("Package not installed: {}", name)); | ||
}; | ||
|
||
// Uninstall the package from the environment. | ||
let uninstall = tokio::task::spawn_blocking({ | ||
let path = dist_info.path().to_owned(); | ||
move || install_wheel_rs::uninstall_wheel(&path) | ||
}) | ||
.await??; | ||
|
||
// Print a summary of the uninstallation. | ||
match (uninstall.file_count, uninstall.dir_count) { | ||
(0, 0) => info!("No files found"), | ||
(1, 0) => info!("Removed 1 file"), | ||
(0, 1) => info!("Removed 1 directory"), | ||
(1, 1) => info!("Removed 1 file and 1 directory"), | ||
(file_count, 0) => info!("Removed {file_count} files"), | ||
(0, dir_count) => info!("Removed {dir_count} directories"), | ||
(file_count, dir_count) => info!("Removed {file_count} files and {dir_count} directories"), | ||
} | ||
|
||
Ok(()) | ||
} |
Oops, something went wrong.