diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index 11cc2ff30b61d..e7849f6cfd9da 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -265,9 +265,41 @@ async fn run(mut cli: Cli) -> Result { if let Some(required_version) = globals.required_version.as_ref() { let package_version = uv_pep440::Version::from_str(uv_version::version())?; if !required_version.contains(&package_version) { - return Err(anyhow::anyhow!( - "Required uv version `{required_version}` does not match the running version `{package_version}`", - )); + // Instead of erroring, try to auto-execute with a compatible version + debug!("Required uv version `{required_version}` does not match the running version `{package_version}`. Attempting to run with a compatible version."); + + // Get the original command line arguments + let args: Vec = std::env::args_os().skip(1).collect(); + + // Reconstruct the command to execute: uv tool run --with 'uv{required_version}' uv ...args + let mut command = std::process::Command::new(std::env::current_exe()?); + command.arg("tool"); + command.arg("run"); + command.arg("--with"); + + // Format the requirement string + let req_string = format!("uv{required_version}"); + command.arg(req_string); + + // Add uv as the command to run + command.arg("uv"); + + // Add all the original arguments + command.args(&args); + + debug!("Executing: {:?}", command); + + // Execute the command and exit with its status + match command.status() { + #[allow(clippy::exit)] + Ok(status) => std::process::exit(status.code().unwrap_or(1)), + Err(e) => { + return Err(anyhow::anyhow!( + "Failed to execute compatible uv version: {}. Original error: Required uv version `{required_version}` does not match the running version `{package_version}`", + e + )); + } + } } } diff --git a/crates/uv/tests/it/main.rs b/crates/uv/tests/it/main.rs index 5ffe6349e7223..e6d5868652755 100644 --- a/crates/uv/tests/it/main.rs +++ b/crates/uv/tests/it/main.rs @@ -118,3 +118,5 @@ mod venv; mod workflow; mod workspace; + +mod required_version; diff --git a/crates/uv/tests/it/required_version.rs b/crates/uv/tests/it/required_version.rs new file mode 100644 index 0000000000000..aaf421a171f04 --- /dev/null +++ b/crates/uv/tests/it/required_version.rs @@ -0,0 +1,86 @@ +use anyhow::Result; +use assert_cmd::Command; +use fs_err::write; +use predicates::prelude::*; +use tempfile::tempdir; + +#[test] +fn auto_exec_with_compatible_version() -> Result<()> { + let temp_dir = tempdir()?; + let project_dir = temp_dir.path(); + + // Create a pyproject.toml with a required-version that doesn't match the current version + // We need to use a valid range that will include the version we can find with `uv tool run` + let pyproject_path = project_dir.join("pyproject.toml"); + write( + &pyproject_path, + r#"[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "test-project" +version = "0.1.0" +requires-python = ">=3.8" + +[tool.uv] +required-version = ">=0.5.0,<0.6.4" +"#, + )?; + + // Run a simple command that should trigger auto-exec + let mut cmd = Command::cargo_bin("uv")?; + cmd.current_dir(project_dir) + .arg("--version") + .env("UV_VERBOSE", "1"); // To get debug output for easier testing + + // The command should succeed and we should get the correct version + let output = cmd.assert().success().get_output().clone(); + let stdout = std::str::from_utf8(&output.stdout).unwrap(); + + // The output will be from the auto-exec call, so it should have the correct version + // We just make sure it contains a version string + assert!(stdout.contains("uv ")); + + Ok(()) +} + +#[test] +fn matches_current_version() -> Result<()> { + let temp_dir = tempdir()?; + let project_dir = temp_dir.path(); + + // Since we can't reliably test a failure case that depends on the environment + // (it could pass if the package for ==0.1.0 is available), we'll just test + // that our current version is used when it matches the required-version. + + // Create a pyproject.toml with a required-version that matches the current version + let pyproject_path = project_dir.join("pyproject.toml"); + let current_version = uv_version::version(); + let pyproject_content = format!( + r#"[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "test-project" +version = "0.1.0" +requires-python = ">=3.8" + +[tool.uv] +required-version = "=={current_version}" +"# + ); + write(&pyproject_path, pyproject_content)?; + + // Run a simple command that should not trigger auto-exec since the version matches + let mut cmd = Command::cargo_bin("uv")?; + cmd.current_dir(project_dir).arg("--version"); + + // The command should succeed with our version + cmd.assert() + .success() + .stdout(predicate::str::contains(current_version)); + + Ok(()) +}