Skip to content

Commit

Permalink
Make uninstall work
Browse files Browse the repository at this point in the history
  • Loading branch information
zanieb committed Aug 19, 2024
1 parent 9920a92 commit e3d3eee
Show file tree
Hide file tree
Showing 9 changed files with 136 additions and 18 deletions.
13 changes: 13 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ dirs-sys = { version = "0.4.1" }
dunce = { version = "1.0.4" }
either = { version = "1.12.0" }
encoding_rs_io = { version = "0.1.7" }
etcetera = { version = "0.8.0" }
flate2 = { version = "1.0.28", default-features = false }
fs-err = { version = "2.11.0" }
fs2 = { version = "0.4.3" }
Expand Down
3 changes: 2 additions & 1 deletion crates/uv-cache/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,13 @@ uv-normalize = { workspace = true }

clap = { workspace = true, features = ["derive", "env"], optional = true }
directories = { workspace = true }
etcetera = { workspace = true }
fs-err = { workspace = true, features = ["tokio"] }
nanoid = { workspace = true }
rmp-serde = { workspace = true }
rustc-hash = { workspace = true }
serde = { workspace = true, features = ["derive"] }
tempfile = { workspace = true }
tracing = { workspace = true }
url = { workspace = true }
walkdir = { workspace = true }
rmp-serde = { workspace = true }
25 changes: 18 additions & 7 deletions crates/uv-cache/src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use std::io;
use std::path::PathBuf;

use crate::Cache;
use clap::Parser;
use directories::ProjectDirs;

use crate::Cache;
use etcetera::BaseStrategy;

#[derive(Parser, Debug, Clone)]
#[command(next_help_heading = "Cache options")]
Expand Down Expand Up @@ -39,13 +39,24 @@ impl Cache {
/// Returns an absolute cache dir.
pub fn from_settings(no_cache: bool, cache_dir: Option<PathBuf>) -> Result<Self, io::Error> {
if no_cache {
Cache::temp()
Self::temp()
} else if let Some(cache_dir) = cache_dir {
Ok(Cache::from_path(cache_dir))
} else if let Some(project_dirs) = ProjectDirs::from("", "", "uv") {
Ok(Cache::from_path(project_dirs.cache_dir()))
Ok(Self::from_path(cache_dir))
} else if let Some(cache_dir) = ProjectDirs::from("", "", "uv")
.map(|dirs| dirs.cache_dir().to_path_buf())
.filter(|dir| dir.exists())
{
// If the user has an existing directory at (e.g.) `/Users/user/Library/Caches/uv`,
// respect it for backwards compatibility. Otherwise, prefer the XDG strategy, even on
// macOS.
Ok(Self::from_path(cache_dir))
} else if let Some(cache_dir) = etcetera::base_strategy::choose_base_strategy()
.ok()
.map(|dirs| dirs.cache_dir().join("uv"))
{
Ok(Self::from_path(cache_dir))
} else {
Ok(Cache::from_path(".uv_cache"))
Ok(Self::from_path(".uv_cache"))
}
}
}
Expand Down
26 changes: 26 additions & 0 deletions crates/uv-fs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,24 @@ pub fn replace_symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io:
}
}

#[cfg(unix)]
pub fn remove_symlink(path: impl AsRef<Path>) -> std::io::Result<()> {
fs_err::remove_file(path.as_ref())
}

#[cfg(windows)]
pub fn remove_symlink(path: impl AsRef<Path>) -> std::io::Result<()> {
match junction::delete(dunce::simplified(path.as_ref())) {
Ok(()) => match fs_err::remove_dir_all(path.as_ref()) {
Ok(()) => Ok(()),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(err) => Err(err),
},
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(err) => Err(err),
}
}

/// Return a [`NamedTempFile`] in the specified directory.
///
/// Sets the permissions of the temporary file to `0o666`, to match the non-temporary file default.
Expand Down Expand Up @@ -283,6 +301,14 @@ pub fn files(path: impl AsRef<Path>) -> impl Iterator<Item = PathBuf> {
.map(|entry| entry.path())
}

/// Returns `true` if a path is a temporary file or directory.
pub fn is_temporary(path: impl AsRef<Path>) -> bool {
path.as_ref()
.file_name()
.and_then(|name| name.to_str())
.map_or(false, |name| name.starts_with(".tmp"))
}

/// A file lock that is automatically released when dropped.
#[derive(Debug)]
pub struct LockedFile(fs_err::File);
Expand Down
1 change: 1 addition & 0 deletions crates/uv-state/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ workspace = true

[dependencies]
directories = { workspace = true }
etcetera = { workspace = true }
tempfile = { workspace = true }
fs-err = { workspace = true }
17 changes: 14 additions & 3 deletions crates/uv-state/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use std::{
};

use directories::ProjectDirs;
use etcetera::BaseStrategy;
use fs_err as fs;
use tempfile::{tempdir, TempDir};

Expand Down Expand Up @@ -83,9 +84,19 @@ impl StateStore {
pub fn from_settings(state_dir: Option<PathBuf>) -> Result<Self, io::Error> {
if let Some(state_dir) = state_dir {
StateStore::from_path(state_dir)
// STOPSHIP: This should use XDG instead...
} else if let Some(project_dirs) = ProjectDirs::from("", "", "uv") {
StateStore::from_path(project_dirs.data_dir())
} else if let Some(data_dir) = ProjectDirs::from("", "", "uv")
.map(|dirs| dirs.data_dir().to_path_buf())
.filter(|dir| dir.exists())
{
// If the user has an existing directory at (e.g.) `/Users/user/Library/Application Support/uv`,
// respect it for backwards compatibility. Otherwise, prefer the XDG strategy, even on
// macOS.
StateStore::from_path(data_dir)
} else if let Some(data_dir) = etcetera::base_strategy::choose_base_strategy()
.ok()
.map(|dirs| dirs.data_dir().join("uv"))
{
StateStore::from_path(data_dir)
} else {
StateStore::from_path(".uv")
}
Expand Down
36 changes: 34 additions & 2 deletions crates/uv/src/commands/python/uninstall.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,42 @@ pub(crate) async fn uninstall(

printer: Printer,
) -> Result<ExitStatus> {
let start = std::time::Instant::now();

let installations = ManagedPythonInstallations::from_settings()?.init()?;
let _lock = installations.acquire_lock()?;

// Perform the uninstallation.
do_uninstall(&installations, targets, all, printer).await?;

// Clean up any empty directories.
if uv_fs::directories(installations.root()).all(|path| uv_fs::is_temporary(&path)) {
fs_err::tokio::remove_dir_all(&installations.root()).await?;

if let Some(top_level) = installations.root().parent() {
// Remove the `toolchains` symlink.
match uv_fs::remove_symlink(top_level.join("toolchains")) {
Ok(()) => {}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
Err(err) => return Err(err.into()),
}

if uv_fs::directories(top_level).all(|path| uv_fs::is_temporary(&path)) {
fs_err::tokio::remove_dir_all(top_level).await?;
}
}
}

Ok(ExitStatus::Success)
}

/// Perform the uninstallation of managed Python installations.
async fn do_uninstall(
installations: &ManagedPythonInstallations,
targets: Vec<String>,
all: bool,
printer: Printer,
) -> Result<ExitStatus> {
let start = std::time::Instant::now();

let requests = if all {
vec![PythonRequest::Any]
} else {
Expand Down Expand Up @@ -108,6 +139,7 @@ pub(crate) async fn uninstall(
}
}

// Report on any uninstalled installations.
if !uninstalled.is_empty() {
if let [uninstalled] = uninstalled.as_slice() {
// Ex) "Uninstalled Python 3.9.7 in 1.68s"
Expand Down
32 changes: 27 additions & 5 deletions crates/uv/src/commands/tool/uninstall.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,28 @@ pub(crate) async fn uninstall(name: Option<PackageName>, printer: Printer) -> Re
Err(err) => return Err(err.into()),
};

// Perform the uninstallation.
do_uninstall(&installed_tools, name, printer).await?;

// Clean up any empty directories.
if uv_fs::directories(installed_tools.root()).all(|path| uv_fs::is_temporary(&path)) {
fs_err::tokio::remove_dir_all(&installed_tools.root()).await?;
if let Some(top_level) = installed_tools.root().parent() {
if uv_fs::directories(top_level).all(|path| uv_fs::is_temporary(&path)) {
fs_err::tokio::remove_dir_all(top_level).await?;
}
}
}

Ok(ExitStatus::Success)
}

/// Perform the uninstallation.
async fn do_uninstall(
installed_tools: &InstalledTools,
name: Option<PackageName>,
printer: Printer,
) -> Result<()> {
let mut dangling = false;
let mut entrypoints = if let Some(name) = name {
let Some(receipt) = installed_tools.get_tool_receipt(&name)? else {
Expand All @@ -37,7 +59,7 @@ pub(crate) async fn uninstall(name: Option<PackageName>, printer: Printer) -> Re
printer.stderr(),
"Removed dangling environment for `{name}`"
)?;
return Ok(ExitStatus::Success);
return Ok(());
}
Err(uv_tool::Error::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => {
bail!("`{name}` is not installed");
Expand All @@ -48,7 +70,7 @@ pub(crate) async fn uninstall(name: Option<PackageName>, printer: Printer) -> Re
}
};

uninstall_tool(&name, &receipt, &installed_tools).await?
uninstall_tool(&name, &receipt, installed_tools).await?
} else {
let mut entrypoints = vec![];
for (name, receipt) in installed_tools.tools()? {
Expand All @@ -72,7 +94,7 @@ pub(crate) async fn uninstall(name: Option<PackageName>, printer: Printer) -> Re
}
};

entrypoints.extend(uninstall_tool(&name, &receipt, &installed_tools).await?);
entrypoints.extend(uninstall_tool(&name, &receipt, installed_tools).await?);
}
entrypoints
};
Expand All @@ -83,7 +105,7 @@ pub(crate) async fn uninstall(name: Option<PackageName>, printer: Printer) -> Re
if !dangling {
writeln!(printer.stderr(), "Nothing to uninstall")?;
}
return Ok(ExitStatus::Success);
return Ok(());
}

let s = if entrypoints.len() == 1 { "" } else { "s" };
Expand All @@ -97,7 +119,7 @@ pub(crate) async fn uninstall(name: Option<PackageName>, printer: Printer) -> Re
.join(", ")
)?;

Ok(ExitStatus::Success)
Ok(())
}

/// Uninstall a tool.
Expand Down

0 comments on commit e3d3eee

Please sign in to comment.