mirror of
https://github.com/astral-sh/uv.git
synced 2025-10-01 22:41:16 +00:00
Remove lingering executables after failed installs (#5666)
## Summary This could still be made more robust, but it's not critical, since you can always `--force`. It's good to handle this case, though, since we have an explicit error for it. Closes https://github.com/astral-sh/uv/issues/5490.
This commit is contained in:
parent
4b8a127c54
commit
f8e2d2f54d
2 changed files with 177 additions and 8 deletions
|
@ -196,10 +196,10 @@ pub(crate) async fn install(
|
||||||
//
|
//
|
||||||
// (If we find existing entrypoints later on, and the tool _doesn't_ exist, we'll avoid removing
|
// (If we find existing entrypoints later on, and the tool _doesn't_ exist, we'll avoid removing
|
||||||
// the external tool's entrypoints (without `--force`).)
|
// the external tool's entrypoints (without `--force`).)
|
||||||
let (existing_tool_receipt, reinstall_entry_points) =
|
let (existing_tool_receipt, invalid_tool_receipt) =
|
||||||
match installed_tools.get_tool_receipt(&from.name) {
|
match installed_tools.get_tool_receipt(&from.name) {
|
||||||
Ok(None) => (None, false),
|
Ok(None) => (None, false),
|
||||||
Ok(Some(receipt)) => (Some(receipt), true),
|
Ok(Some(receipt)) => (Some(receipt), false),
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
// If the tool is not installed properly, remove the environment and continue.
|
// If the tool is not installed properly, remove the environment and continue.
|
||||||
match installed_tools.remove_environment(&from.name) {
|
match installed_tools.remove_environment(&from.name) {
|
||||||
|
@ -276,7 +276,7 @@ pub(crate) async fn install(
|
||||||
// entrypoints always contain an absolute path to the relevant Python interpreter, which would
|
// entrypoints always contain an absolute path to the relevant Python interpreter, which would
|
||||||
// be invalidated by moving the environment.
|
// be invalidated by moving the environment.
|
||||||
let environment = if let Some(environment) = existing_environment {
|
let environment = if let Some(environment) = existing_environment {
|
||||||
update_environment(
|
let environment = update_environment(
|
||||||
environment,
|
environment,
|
||||||
spec,
|
spec,
|
||||||
&settings,
|
&settings,
|
||||||
|
@ -288,7 +288,15 @@ pub(crate) async fn install(
|
||||||
cache,
|
cache,
|
||||||
printer,
|
printer,
|
||||||
)
|
)
|
||||||
.await?
|
.await?;
|
||||||
|
|
||||||
|
// At this point, we updated the existing environment, so we should remove any of its
|
||||||
|
// existing executables.
|
||||||
|
if let Some(existing_receipt) = existing_tool_receipt {
|
||||||
|
remove_entrypoints(&existing_receipt);
|
||||||
|
}
|
||||||
|
|
||||||
|
environment
|
||||||
} else {
|
} else {
|
||||||
// If we're creating a new environment, ensure that we can resolve the requirements prior
|
// If we're creating a new environment, ensure that we can resolve the requirements prior
|
||||||
// to removing any existing tools.
|
// to removing any existing tools.
|
||||||
|
@ -308,6 +316,12 @@ pub(crate) async fn install(
|
||||||
|
|
||||||
let environment = installed_tools.create_environment(&from.name, interpreter)?;
|
let environment = installed_tools.create_environment(&from.name, interpreter)?;
|
||||||
|
|
||||||
|
// At this point, we removed any existing environment, so we should remove any of its
|
||||||
|
// executables.
|
||||||
|
if let Some(existing_receipt) = existing_tool_receipt {
|
||||||
|
remove_entrypoints(&existing_receipt);
|
||||||
|
}
|
||||||
|
|
||||||
// Sync the environment with the resolved requirements.
|
// Sync the environment with the resolved requirements.
|
||||||
sync_environment(
|
sync_environment(
|
||||||
environment,
|
environment,
|
||||||
|
@ -370,8 +384,9 @@ pub(crate) async fn install(
|
||||||
|
|
||||||
hint_executable_from_dependency(&from, &environment, printer)?;
|
hint_executable_from_dependency(&from, &environment, printer)?;
|
||||||
|
|
||||||
// Clean up the environment we just created
|
// Clean up the environment we just created.
|
||||||
installed_tools.remove_environment(&from.name)?;
|
installed_tools.remove_environment(&from.name)?;
|
||||||
|
|
||||||
return Ok(ExitStatus::Failure);
|
return Ok(ExitStatus::Failure);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -381,9 +396,9 @@ pub(crate) async fn install(
|
||||||
.filter(|(_, _, target_path)| target_path.exists())
|
.filter(|(_, _, target_path)| target_path.exists())
|
||||||
.peekable();
|
.peekable();
|
||||||
|
|
||||||
// Note we use `reinstall_entry_points` here instead of `reinstall`; requesting reinstall
|
// Ignore any existing entrypoints if the user passed `--force`, or the existing recept was
|
||||||
// will _not_ remove existing entry points when they are not managed by uv.
|
// broken.
|
||||||
if force || reinstall_entry_points {
|
if force || invalid_tool_receipt {
|
||||||
for (name, _, target) in existing_entry_points {
|
for (name, _, target) in existing_entry_points {
|
||||||
debug!("Removing existing executable: `{name}`");
|
debug!("Removing existing executable: `{name}`");
|
||||||
fs_err::remove_file(target)?;
|
fs_err::remove_file(target)?;
|
||||||
|
@ -478,6 +493,23 @@ pub(crate) async fn install(
|
||||||
Ok(ExitStatus::Success)
|
Ok(ExitStatus::Success)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Remove any entrypoints attached to the [`Tool`].
|
||||||
|
fn remove_entrypoints(tool: &Tool) {
|
||||||
|
for executable in tool
|
||||||
|
.entrypoints()
|
||||||
|
.iter()
|
||||||
|
.map(|entrypoint| &entrypoint.install_path)
|
||||||
|
{
|
||||||
|
debug!("Removing executable: `{}`", executable.simplified_display());
|
||||||
|
if let Err(err) = fs_err::remove_file(executable) {
|
||||||
|
warn!(
|
||||||
|
"Failed to remove executable: `{}`: {err}",
|
||||||
|
executable.simplified_display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Displays a hint if an executable matching the package name can be found in a dependency of the package.
|
/// Displays a hint if an executable matching the package name can be found in a dependency of the package.
|
||||||
fn hint_executable_from_dependency(
|
fn hint_executable_from_dependency(
|
||||||
from: &Requirement,
|
from: &Requirement,
|
||||||
|
|
|
@ -467,6 +467,143 @@ fn tool_install_editable() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ensure that we remove any existing entrypoints upon error.
|
||||||
|
#[test]
|
||||||
|
fn tool_install_remove_on_empty() -> Result<()> {
|
||||||
|
let context = TestContext::new("3.12").with_filtered_exe_suffix();
|
||||||
|
let tool_dir = context.temp_dir.child("tools");
|
||||||
|
let bin_dir = context.temp_dir.child("bin");
|
||||||
|
|
||||||
|
// Request `black`. It should reinstall from the registry.
|
||||||
|
uv_snapshot!(context.filters(), context.tool_install()
|
||||||
|
.arg("black")
|
||||||
|
.env("UV_TOOL_DIR", tool_dir.as_os_str())
|
||||||
|
.env("XDG_BIN_HOME", bin_dir.as_os_str())
|
||||||
|
.env("PATH", bin_dir.as_os_str()), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
warning: `uv tool install` is experimental and may change without warning
|
||||||
|
Resolved 6 packages in [TIME]
|
||||||
|
Prepared 6 packages in [TIME]
|
||||||
|
Installed 6 packages in [TIME]
|
||||||
|
+ black==24.3.0
|
||||||
|
+ click==8.1.7
|
||||||
|
+ mypy-extensions==1.0.0
|
||||||
|
+ packaging==24.0
|
||||||
|
+ pathspec==0.12.1
|
||||||
|
+ platformdirs==4.2.0
|
||||||
|
Installed 2 executables: black, blackd
|
||||||
|
"###);
|
||||||
|
|
||||||
|
insta::with_settings!({
|
||||||
|
filters => context.filters(),
|
||||||
|
}, {
|
||||||
|
// We should have a tool receipt
|
||||||
|
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r###"
|
||||||
|
[tool]
|
||||||
|
requirements = [{ name = "black" }]
|
||||||
|
entrypoints = [
|
||||||
|
{ name = "black", install-path = "[TEMP_DIR]/bin/black" },
|
||||||
|
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
|
||||||
|
]
|
||||||
|
"###);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Install `black` as an editable package, but without any entrypoints.
|
||||||
|
let black = context.temp_dir.child("black");
|
||||||
|
fs_err::create_dir_all(black.path())?;
|
||||||
|
|
||||||
|
let pyproject_toml = black.child("pyproject.toml");
|
||||||
|
pyproject_toml.write_str(indoc! {r#"
|
||||||
|
[project]
|
||||||
|
name = "black"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Black without any entrypoints"
|
||||||
|
authors = []
|
||||||
|
dependencies = []
|
||||||
|
requires-python = ">=3.11,<3.13"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
"#
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let src = black.child("src").child("black");
|
||||||
|
fs_err::create_dir_all(src.path())?;
|
||||||
|
|
||||||
|
let init = src.child("__init__.py");
|
||||||
|
init.touch()?;
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.tool_install()
|
||||||
|
.arg("-e")
|
||||||
|
.arg(black.path())
|
||||||
|
.env("UV_TOOL_DIR", tool_dir.as_os_str())
|
||||||
|
.env("XDG_BIN_HOME", bin_dir.as_os_str())
|
||||||
|
.env("PATH", bin_dir.as_os_str()), @r###"
|
||||||
|
success: false
|
||||||
|
exit_code: 1
|
||||||
|
----- stdout -----
|
||||||
|
No executables are provided by `black`
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
warning: `uv tool install` is experimental and may change without warning
|
||||||
|
Resolved 1 package in [TIME]
|
||||||
|
Prepared 1 package in [TIME]
|
||||||
|
Uninstalled 6 packages in [TIME]
|
||||||
|
Installed 1 package in [TIME]
|
||||||
|
- black==24.3.0
|
||||||
|
+ black==0.1.0 (from file://[TEMP_DIR]/black)
|
||||||
|
- click==8.1.7
|
||||||
|
- mypy-extensions==1.0.0
|
||||||
|
- packaging==24.0
|
||||||
|
- pathspec==0.12.1
|
||||||
|
- platformdirs==4.2.0
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// Re-request `black`. It should reinstall, without requiring `--force`.
|
||||||
|
uv_snapshot!(context.filters(), context.tool_install()
|
||||||
|
.arg("black")
|
||||||
|
.env("UV_TOOL_DIR", tool_dir.as_os_str())
|
||||||
|
.env("XDG_BIN_HOME", bin_dir.as_os_str())
|
||||||
|
.env("PATH", bin_dir.as_os_str()), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
warning: `uv tool install` is experimental and may change without warning
|
||||||
|
Resolved 6 packages in [TIME]
|
||||||
|
Installed 6 packages in [TIME]
|
||||||
|
+ black==24.3.0
|
||||||
|
+ click==8.1.7
|
||||||
|
+ mypy-extensions==1.0.0
|
||||||
|
+ packaging==24.0
|
||||||
|
+ pathspec==0.12.1
|
||||||
|
+ platformdirs==4.2.0
|
||||||
|
Installed 2 executables: black, blackd
|
||||||
|
"###);
|
||||||
|
|
||||||
|
insta::with_settings!({
|
||||||
|
filters => context.filters(),
|
||||||
|
}, {
|
||||||
|
// We should have a tool receipt
|
||||||
|
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r###"
|
||||||
|
[tool]
|
||||||
|
requirements = [{ name = "black" }]
|
||||||
|
entrypoints = [
|
||||||
|
{ name = "black", install-path = "[TEMP_DIR]/bin/black" },
|
||||||
|
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
|
||||||
|
]
|
||||||
|
"###);
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Test an editable installation of a tool using `--from`.
|
/// Test an editable installation of a tool using `--from`.
|
||||||
#[test]
|
#[test]
|
||||||
fn tool_install_editable_from() {
|
fn tool_install_editable_from() {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue