Detect cases where uv partially removed a virtual environment and retry

This commit is contained in:
Zanie Blue 2025-07-11 13:25:47 -05:00
parent 081e2010df
commit 89edcbefd3
2 changed files with 76 additions and 2 deletions

View File

@ -920,7 +920,9 @@ impl ProjectInterpreter {
));
}
InvalidEnvironmentKind::MissingExecutable(_) => {
if fs_err::read_dir(&root).is_ok_and(|mut dir| dir.next().is_some()) {
if fs_err::read_dir(&root).is_ok_and(|mut dir| dir.next().is_some())
&& !root.join(".uv-partial-rm").try_exists().unwrap_or(false)
{
return Err(ProjectError::InvalidProjectEnvironmentDir(
root,
"it is not a valid Python environment (no Python executable was found)"
@ -1294,6 +1296,9 @@ impl ProjectEnvironment {
// Unless it's empty, in which case we just ignore it
if root.read_dir().is_ok_and(|mut dir| dir.next().is_none()) {
false
// Or, if there's a marker file indicating a previous removal failed
} else if root.join(".uv-partial-rm").try_exists().unwrap_or(false) {
true
} else {
return Err(ProjectError::InvalidProjectEnvironmentDir(
root,
@ -1363,7 +1368,23 @@ impl ProjectEnvironment {
)?;
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
Err(e) => return Err(e.into()),
Err(e) => {
// Try to write a marker to indicate that we failed to remove the entire
// environment, so when the next command runs we can attempt to remove
// it even if a `pyvenv.cfg` marker was removed
match fs_err::write(root.join(".uv-partial-rm"), "") {
Ok(()) => {
debug!(
"Wrote `.uv-partial-rm` marker to partially deleted environment: {}",
root.user_display().cyan()
);
}
Err(err) => {
debug!("Failed to write `.uv-partial-rm` marker: {err}");
}
}
return Err(e.into());
}
}
}

View File

@ -5000,6 +5000,59 @@ fn sync_update_project() -> Result<()> {
Ok(())
}
#[test]
fn sync_no_pyvenv_cfg() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "my-project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["iniconfig"]
"#,
)?;
// Break the virtual environment
fs_err::remove_dir_all(
context
.venv
.join(if cfg!(windows) { "Scripts" } else { "bin" }),
)?;
fs_err::remove_file(context.venv.join("pyvenv.cfg"))?;
// Running `uv sync` won't create the venv
uv_snapshot!(context.filters(), context.sync(), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Project virtual environment directory `[VENV]/` cannot be used because it is not a valid Python environment (no Python executable was found)
");
// Write the marker that indicates the environment was partially removed by uv
fs_err::write(context.venv.join(".uv-partial-rm"), "")?;
uv_snapshot!(context.filters(), context.sync(), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Removed virtual environment at: .venv
Creating virtual environment at: .venv
Resolved 2 packages in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ iniconfig==2.0.0
");
Ok(())
}
#[test]
fn sync_environment_prompt() -> Result<()> {
let context = TestContext::new_with_versions(&["3.12"]);