Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 35 additions & 3 deletions crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -265,9 +265,41 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
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<OsString> = 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
));
}
}
}
}

Expand Down
2 changes: 2 additions & 0 deletions crates/uv/tests/it/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,5 @@ mod venv;
mod workflow;

mod workspace;

mod required_version;
86 changes: 86 additions & 0 deletions crates/uv/tests/it/required_version.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
Loading