mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 21:35:00 +00:00
Replace tool environments on updated Python request (#4746)
## Summary Closes https://github.com/astral-sh/uv/issues/4741.
This commit is contained in:
parent
e88e1373e6
commit
75731452d8
3 changed files with 158 additions and 32 deletions
|
@ -147,6 +147,10 @@ impl InstalledTools {
|
||||||
/// Remove the environment for a tool.
|
/// Remove the environment for a tool.
|
||||||
///
|
///
|
||||||
/// Does not remove the tool's entrypoints.
|
/// Does not remove the tool's entrypoints.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// If no such environment exists for the tool.
|
||||||
pub fn remove_environment(&self, name: &PackageName) -> Result<(), Error> {
|
pub fn remove_environment(&self, name: &PackageName) -> Result<(), Error> {
|
||||||
let _lock = self.acquire_lock();
|
let _lock = self.acquire_lock();
|
||||||
let environment_path = self.root.join(name.to_string());
|
let environment_path = self.root.join(name.to_string());
|
||||||
|
@ -161,23 +165,45 @@ impl InstalledTools {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the [`PythonEnvironment`] for a given tool.
|
/// Return the [`PythonEnvironment`] for a given tool, if it exists.
|
||||||
pub fn environment(
|
pub fn get_environment(
|
||||||
&self,
|
&self,
|
||||||
name: &PackageName,
|
name: &PackageName,
|
||||||
remove_existing: bool,
|
|
||||||
interpreter: Interpreter,
|
|
||||||
cache: &Cache,
|
cache: &Cache,
|
||||||
) -> Result<PythonEnvironment, Error> {
|
) -> Result<Option<PythonEnvironment>, Error> {
|
||||||
let _lock = self.acquire_lock();
|
let _lock = self.acquire_lock();
|
||||||
let environment_path = self.root.join(name.to_string());
|
let environment_path = self.root.join(name.to_string());
|
||||||
|
|
||||||
if !remove_existing && environment_path.exists() {
|
if environment_path.is_dir() {
|
||||||
debug!(
|
debug!(
|
||||||
"Using existing environment for tool `{name}` at `{}`.",
|
"Using existing environment for tool `{name}` at `{}`.",
|
||||||
environment_path.user_display()
|
environment_path.user_display()
|
||||||
);
|
);
|
||||||
return Ok(PythonEnvironment::from_root(environment_path, cache)?);
|
Ok(Some(PythonEnvironment::from_root(environment_path, cache)?))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create the [`PythonEnvironment`] for a given tool, removing any existing environments.
|
||||||
|
pub fn create_environment(
|
||||||
|
&self,
|
||||||
|
name: &PackageName,
|
||||||
|
interpreter: Interpreter,
|
||||||
|
) -> Result<PythonEnvironment, Error> {
|
||||||
|
let _lock = self.acquire_lock();
|
||||||
|
let environment_path = self.root.join(name.to_string());
|
||||||
|
|
||||||
|
// Remove any existing environment.
|
||||||
|
match fs_err::remove_dir_all(&environment_path) {
|
||||||
|
Ok(()) => {
|
||||||
|
debug!(
|
||||||
|
"Removed existing environment for tool `{name}` at `{}`.",
|
||||||
|
environment_path.user_display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(err) if err.kind() == io::ErrorKind::NotFound => (),
|
||||||
|
Err(err) => return Err(err.into()),
|
||||||
}
|
}
|
||||||
|
|
||||||
debug!(
|
debug!(
|
||||||
|
|
|
@ -56,8 +56,12 @@ pub(crate) async fn install(
|
||||||
.connectivity(connectivity)
|
.connectivity(connectivity)
|
||||||
.native_tls(native_tls);
|
.native_tls(native_tls);
|
||||||
|
|
||||||
|
let python_request = python.as_deref().map(ToolchainRequest::parse);
|
||||||
|
|
||||||
|
// Pre-emptively identify a Python interpreter. We need an interpreter to resolve any unnamed
|
||||||
|
// requirements, even if we end up using a different interpreter for the tool install itself.
|
||||||
let interpreter = Toolchain::find_or_fetch(
|
let interpreter = Toolchain::find_or_fetch(
|
||||||
python.as_deref().map(ToolchainRequest::parse),
|
python_request.clone(),
|
||||||
EnvironmentPreference::OnlySystem,
|
EnvironmentPreference::OnlySystem,
|
||||||
toolchain_preference,
|
toolchain_preference,
|
||||||
toolchain_fetch,
|
toolchain_fetch,
|
||||||
|
@ -147,21 +151,42 @@ pub(crate) async fn install(
|
||||||
|
|
||||||
let installed_tools = InstalledTools::from_settings()?;
|
let installed_tools = InstalledTools::from_settings()?;
|
||||||
let existing_tool_receipt = installed_tools.get_tool_receipt(&from.name)?;
|
let existing_tool_receipt = installed_tools.get_tool_receipt(&from.name)?;
|
||||||
|
let existing_environment =
|
||||||
|
installed_tools
|
||||||
|
.get_environment(&from.name, cache)?
|
||||||
|
.filter(|environment| {
|
||||||
|
python_request.as_ref().map_or(true, |python_request| {
|
||||||
|
if python_request.satisfied(environment.interpreter(), cache) {
|
||||||
|
debug!("Found existing environment for tool `{}`", from.name);
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
let _ = writeln!(
|
||||||
|
printer.stderr(),
|
||||||
|
"Existing environment for `{}` does not satisfy the requested Python interpreter: `{}`",
|
||||||
|
from.name,
|
||||||
|
python_request
|
||||||
|
);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
// If the requested and receipt requirements are the same...
|
// If the requested and receipt requirements are the same...
|
||||||
if let Some(tool_receipt) = existing_tool_receipt.as_ref() {
|
if existing_environment.is_some() {
|
||||||
let receipt = tool_receipt
|
if let Some(tool_receipt) = existing_tool_receipt.as_ref() {
|
||||||
.requirements()
|
let receipt = tool_receipt
|
||||||
.iter()
|
.requirements()
|
||||||
.cloned()
|
.iter()
|
||||||
.map(Requirement::from)
|
.cloned()
|
||||||
.collect::<Vec<_>>();
|
.map(Requirement::from)
|
||||||
if requirements == receipt {
|
.collect::<Vec<_>>();
|
||||||
// And the user didn't request a reinstall or upgrade...
|
if requirements == receipt {
|
||||||
if !force && settings.reinstall.is_none() && settings.upgrade.is_none() {
|
// And the user didn't request a reinstall or upgrade...
|
||||||
// We're done.
|
if !force && settings.reinstall.is_none() && settings.upgrade.is_none() {
|
||||||
writeln!(printer.stderr(), "Tool `{from}` is already installed")?;
|
// We're done.
|
||||||
return Ok(ExitStatus::Failure);
|
writeln!(printer.stderr(), "Tool `{from}` is already installed")?;
|
||||||
|
return Ok(ExitStatus::Failure);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -171,9 +196,15 @@ pub(crate) async fn install(
|
||||||
// entrypoints (without `--force`).
|
// entrypoints (without `--force`).
|
||||||
let reinstall_entry_points = existing_tool_receipt.is_some();
|
let reinstall_entry_points = existing_tool_receipt.is_some();
|
||||||
|
|
||||||
// TODO(zanieb): Build the environment in the cache directory then copy into the tool directory
|
// TODO(zanieb): Build the environment in the cache directory then copy into the tool directory.
|
||||||
// This lets us confirm the environment is valid before removing an existing install
|
// This lets us confirm the environment is valid before removing an existing install. However,
|
||||||
let environment = installed_tools.environment(&from.name, force, interpreter, cache)?;
|
// entrypoints always contain an absolute path to the relevant Python interpreter, which would
|
||||||
|
// be invalidated by moving the environment.
|
||||||
|
let environment = if let Some(environment) = existing_environment {
|
||||||
|
environment
|
||||||
|
} else {
|
||||||
|
installed_tools.create_environment(&from.name, interpreter)?
|
||||||
|
};
|
||||||
|
|
||||||
// Install the ephemeral requirements.
|
// Install the ephemeral requirements.
|
||||||
let spec = RequirementsSpecification::from_requirements(requirements.clone());
|
let spec = RequirementsSpecification::from_requirements(requirements.clone());
|
||||||
|
|
|
@ -648,14 +648,6 @@ fn tool_install_entry_point_exists() {
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
warning: `uv tool install` is experimental and may change without warning.
|
warning: `uv tool install` is experimental and may change without warning.
|
||||||
Resolved [N] packages in [TIME]
|
|
||||||
Installed [N] 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: black, blackd
|
Installed: black, blackd
|
||||||
"###);
|
"###);
|
||||||
|
|
||||||
|
@ -1329,3 +1321,80 @@ fn tool_install_upgrade() {
|
||||||
"###);
|
"###);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Test reinstalling tools with varying `--python` requests.
|
||||||
|
#[test]
|
||||||
|
fn tool_install_python_request() {
|
||||||
|
let context = TestContext::new_with_versions(&["3.11", "3.12"])
|
||||||
|
.with_filtered_counts()
|
||||||
|
.with_filtered_exe_suffix();
|
||||||
|
let tool_dir = context.temp_dir.child("tools");
|
||||||
|
let bin_dir = context.temp_dir.child("bin");
|
||||||
|
|
||||||
|
// Install `black`.
|
||||||
|
uv_snapshot!(context.filters(), context.tool_install()
|
||||||
|
.arg("-p")
|
||||||
|
.arg("3.12")
|
||||||
|
.arg("black")
|
||||||
|
.env("UV_TOOL_DIR", tool_dir.as_os_str())
|
||||||
|
.env("XDG_BIN_HOME", 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 [N] packages in [TIME]
|
||||||
|
Prepared [N] packages in [TIME]
|
||||||
|
Installed [N] 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: black, blackd
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// Install with Python 3.12 (compatible).
|
||||||
|
uv_snapshot!(context.filters(), context.tool_install()
|
||||||
|
.arg("-p")
|
||||||
|
.arg("3.12")
|
||||||
|
.arg("black")
|
||||||
|
.env("UV_TOOL_DIR", tool_dir.as_os_str())
|
||||||
|
.env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###"
|
||||||
|
success: false
|
||||||
|
exit_code: 1
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
warning: `uv tool install` is experimental and may change without warning.
|
||||||
|
Tool `black` is already installed
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// Install with Python 3.11 (incompatible).
|
||||||
|
uv_snapshot!(context.filters(), context.tool_install()
|
||||||
|
.arg("-p")
|
||||||
|
.arg("3.11")
|
||||||
|
.arg("black")
|
||||||
|
.env("UV_TOOL_DIR", tool_dir.as_os_str())
|
||||||
|
.env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
warning: `uv tool install` is experimental and may change without warning.
|
||||||
|
Existing environment for `black` does not satisfy the requested Python interpreter: `Python 3.11`
|
||||||
|
Resolved [N] packages in [TIME]
|
||||||
|
Prepared [N] packages in [TIME]
|
||||||
|
Installed [N] 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: black, blackd
|
||||||
|
"###);
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue