From dd9f7603897979d1ea4b225b2b7980267317cb14 Mon Sep 17 00:00:00 2001 From: David Peter Date: Mon, 28 Sep 2026 12:15:47 +0200 Subject: [PATCH 1/4] [ty] Explain outdated uv versions in metadata errors --- crates/ty/tests/cli/scripts.rs | 47 +++++ crates/ty_project/src/uv/command.rs | 221 ++++++++++++++++++++++- crates/ty_project/src/uv/environments.rs | 6 +- crates/ty_project/src/uv/metadata.rs | 8 + crates/ty_project/src/uv/service.rs | 8 +- 5 files changed, 276 insertions(+), 14 deletions(-) diff --git a/crates/ty/tests/cli/scripts.rs b/crates/ty/tests/cli/scripts.rs index 1e3a7ca5e7702..5a39a8675af83 100644 --- a/crates/ty/tests/cli/scripts.rs +++ b/crates/ty/tests/cli/scripts.rs @@ -1,3 +1,6 @@ +#[cfg(unix)] +use std::{fs, os::unix::fs::PermissionsExt}; + use insta_cmd::assert_cmd_snapshot; use ty_static::EnvVars; @@ -1352,6 +1355,50 @@ fn unavailable_uv_reports_metadata_error() -> anyhow::Result<()> { Ok(()) } +#[test] +#[cfg(unix)] +fn old_uv_reports_upgrade() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ("script.py", "# /// script\n# dependencies = []\n# ///\n"), + ( + "uv", + "#!/bin/sh\nif [ \"$1\" = '--version' ]; then echo 'uv 0.12.2'; fi\n", + ), + ])?; + let uv = case.root().join("uv"); + fs::set_permissions(&uv, fs::Permissions::from_mode(0o755))?; + + assert_cmd_snapshot!( + case.command().arg("script.py").env(EnvVars::TY_UV, "scripts").env(EnvVars::UV, &uv), + @" + success: false + exit_code: 1 + ----- stdout ----- + error[uv-metadata]: uv 0.12.2 is too old; upgrade `/uv` to uv 0.12.3 or newer + --> script.py:1:1 + + Found 1 diagnostic + + ----- stderr ----- + " + ); + assert_cmd_snapshot!( + case.command().arg("script.py").args(["--output-format", "concise"]) + .env(EnvVars::TY_UV, "scripts").env(EnvVars::UV, &uv), + @" + success: false + exit_code: 1 + ----- stdout ----- + script.py:1:1: error[uv-metadata] uv 0.12.2 is too old; upgrade `/uv` to uv 0.12.3 or newer + Found 1 diagnostic + + ----- stderr ----- + " + ); + + Ok(()) +} + #[test] fn ordinary_files_do_not_initialize_scripts() -> anyhow::Result<()> { let case = CliTest::with_files([ diff --git a/crates/ty_project/src/uv/command.rs b/crates/ty_project/src/uv/command.rs index a2f8edcd9cf09..e425476c7d8e5 100644 --- a/crates/ty_project/src/uv/command.rs +++ b/crates/ty_project/src/uv/command.rs @@ -2,6 +2,7 @@ use std::process::Output; +use pep440_rs::Version; use ruff_db::system::{Command, CommandExecutor, System, SystemPath, WhichError}; use ty_static::EnvVars; @@ -31,11 +32,12 @@ impl Uv { let output = system .command_executor() .ok_or_else(unsupported_command_execution) + .map_err(UvMetadataError::Invocation) .and_then(|executor| self.execute(executor, target)); Self::parse_metadata_output(system, output) } - /// Executes `uv workspace metadata` without interpreting its output. + /// Executes `uv workspace metadata`, checking for an unsupported uv version on failure. /// /// This operation only requires a detached command executor, so it can run on a background /// worker. @@ -44,7 +46,7 @@ impl Uv { &self, executor: &dyn CommandExecutor, target: &MetadataTarget<'_>, - ) -> std::io::Result { + ) -> Result { let mut command = Command::new(self.executable.as_str()); command.args(["workspace", "metadata", "--quiet"]); @@ -79,15 +81,60 @@ impl Uv { start.elapsed().as_secs_f64() ); - output + let output = output.map_err(UvMetadataError::Invocation)?; + + // Before uv 0.12.3, `--quiet` suppresses the metadata JSON even on success. + // Only probe the version when metadata is unavailable, so successful calls stay cheap. + if (!output.status.success() || output.stdout.is_empty()) + && let Some(version) = self.version(executor, target) + { + let minimum_version = Version::new([0, 12, 3]); + if version < minimum_version { + return Err(UvMetadataError::UnsupportedVersion { + executable: self.executable.clone(), + version, + minimum_version, + }); + } + } + + Ok(output) + } + + fn version( + &self, + executor: &dyn CommandExecutor, + target: &MetadataTarget<'_>, + ) -> Option { + let mut command = Command::new(self.executable.as_str()); + command.arg("--version"); + let directory = match target { + MetadataTarget::Workspace(path) => Some(*path), + MetadataTarget::Script { path, .. } => path.parent(), + }; + if let Some(directory) = directory { + command.current_dir(directory); + } + let output = executor.execute(command).ok()?; + if !output.status.success() { + return None; + } + + std::str::from_utf8(&output.stdout) + .ok()? + .strip_prefix("uv ")? + .split_whitespace() + .next()? + .parse() + .ok() } /// Parses and validates the output returned by [`Self::execute`]. pub(crate) fn parse_metadata_output( system: &dyn System, - output: std::io::Result, + output: Result, ) -> Result { - let output = output.map_err(UvMetadataError::Invocation)?; + let output = output?; if !output.status.success() { return Err(UvMetadataError::CommandFailed { @@ -130,10 +177,47 @@ pub(super) fn unsupported_command_execution() -> std::io::Error { #[cfg(test)] mod tests { - use ruff_db::system::TestSystem; + use std::collections::VecDeque; + #[cfg(unix)] + use std::os::unix::process::ExitStatusExt; + #[cfg(windows)] + use std::os::windows::process::ExitStatusExt; + use std::process::{ExitStatus, Output}; + use std::sync::Arc; + + use parking_lot::Mutex; + use ruff_db::system::{Command, CommandExecutor, SystemPath, TestSystem}; use ty_static::EnvVars; - use super::Uv; + use super::{MetadataTarget, Uv, UvMetadataError}; + + #[derive(Clone, Default)] + struct MockExecutor { + outputs: Arc>>>, + commands: Arc>>, + } + + impl CommandExecutor for MockExecutor { + fn execute(&self, command: Command) -> std::io::Result { + self.commands.lock().push(command); + self.outputs + .lock() + .pop_front() + .unwrap_or_else(|| Err(std::io::Error::other("unexpected command"))) + } + + fn dyn_clone(&self) -> Box { + Box::new(self.clone()) + } + } + + fn output(success: bool, stdout: &str, stderr: &str) -> Output { + Output { + status: ExitStatus::from_raw((!success).into()), + stdout: stdout.as_bytes().to_vec(), + stderr: stderr.as_bytes().to_vec(), + } + } #[test] fn explicit_uv_override_skips_path_lookup() -> anyhow::Result<()> { @@ -146,4 +230,127 @@ mod tests { Ok(()) } + + #[test] + fn successful_metadata_does_not_probe_version() -> anyhow::Result<()> { + let system = TestSystem::default(); + let root = SystemPath::new(if cfg!(windows) { "C:/app" } else { "/app" }); + system.memory_file_system().create_directory_all(root)?; + let metadata = serde_json::json!({ + "schema": {"version": "preview"}, + "workspace_root": root.as_str(), + }); + let executor = MockExecutor::default(); + executor + .outputs + .lock() + .push_back(Ok(output(true, &metadata.to_string(), ""))); + let uv = Uv { + executable: "custom-uv".into(), + }; + + let result = uv.execute(&executor, &MetadataTarget::Workspace(root)); + assert_eq!( + Uv::parse_metadata_output(&system, result)?.workspace_root(), + root + ); + assert_eq!(executor.commands.lock().len(), 1); + + Ok(()) + } + + #[test] + fn old_uv_reports_upgrade() { + // Old uv can either reject the metadata command or succeed with no JSON due to --quiet. + for metadata in [ + output(false, "", "unrecognized subcommand"), + output(true, "", ""), + ] { + let executor = MockExecutor::default(); + executor.outputs.lock().extend([ + Ok(metadata), + Ok(output(true, "uv 0.12.2 (abcdef 2026-08-05)\n", "")), + ]); + let uv = Uv { + executable: "custom-uv".into(), + }; + let result = uv.execute( + &executor, + &MetadataTarget::Script { + path: SystemPath::new("/app/script.py"), + python: None, + }, + ); + + let error = result.unwrap_err(); + assert_eq!( + error.to_string(), + "uv 0.12.2 is too old; upgrade `custom-uv` to uv 0.12.3 or newer" + ); + let commands = executor.commands.lock(); + assert_eq!(commands.len(), 2); + assert!( + commands + .iter() + .all(|command| command.get_executable() == "custom-uv") + ); + assert_eq!(commands[1].get_args(), ["--version"]); + assert_eq!(commands[1].get_current_dir(), commands[0].get_current_dir()); + } + } + + #[test] + fn preserves_errors_without_an_unsupported_version() { + for version in [ + Ok(output(true, "uv 0.12.3\n", "")), + Ok(output(true, "uv 0.12.19 (abcdef 2026-09-24)\n", "")), + Ok(output(true, "uv 1.0.0\n", "")), + Ok(output(true, "unknown version\n", "")), + Ok(output(false, "uv 0.12.2\n", "version failed")), + Err(std::io::Error::other("version unavailable")), + ] { + let executor = MockExecutor::default(); + let metadata = output(false, "", "dependency could not be resolved"); + let status = metadata.status; + executor.outputs.lock().extend([Ok(metadata), version]); + let uv = Uv { + executable: "custom-uv".into(), + }; + let result = uv.execute( + &executor, + &MetadataTarget::Workspace(SystemPath::new("/app")), + ); + let error = Uv::parse_metadata_output(&TestSystem::default(), result).unwrap_err(); + assert!( + matches!(error, UvMetadataError::CommandFailed { status: actual, stderr } + if actual == status && stderr == "dependency could not be resolved") + ); + } + } + + #[test] + fn preserves_invalid_metadata_from_supported_uv() { + for stdout in ["", "invalid JSON"] { + let executor = MockExecutor::default(); + executor.outputs.lock().extend([ + Ok(output(true, stdout, "")), + Ok(output(true, "uv 0.12.3\n", "")), + ]); + let uv = Uv { + executable: "custom-uv".into(), + }; + let result = uv.execute( + &executor, + &MetadataTarget::Workspace(SystemPath::new("/app")), + ); + assert!(matches!( + Uv::parse_metadata_output(&TestSystem::default(), result), + Err(UvMetadataError::InvalidMetadata(_)) + )); + assert_eq!( + executor.commands.lock().len(), + if stdout.is_empty() { 2 } else { 1 } + ); + } + } } diff --git a/crates/ty_project/src/uv/environments.rs b/crates/ty_project/src/uv/environments.rs index 3ab21c2b79dfc..7c73d3a790cea 100644 --- a/crates/ty_project/src/uv/environments.rs +++ b/crates/ty_project/src/uv/environments.rs @@ -81,8 +81,8 @@ use salsa::Setter; use crate::script::script_tag; use crate::uv::{ - ScriptSyncRequest, ScriptSyncTask, Uv, UvMetadata, UvMetadataResult, UvMetadataService, - UvSyncTask, + ScriptSyncRequest, ScriptSyncTask, Uv, UvMetadata, UvMetadataError, UvMetadataResult, + UvMetadataService, UvSyncTask, }; use crate::{Db, ProjectReloadResult, ProjectSyncProgressFactory, UseUv, UvSyncProgress}; @@ -710,7 +710,7 @@ fn apply_sync_result( db: &mut dyn Db, environment: ScriptEnvironment, request: &ScriptSyncRequest, - output: std::io::Result, + output: Result, ) { let (uv_metadata, initialization_error) = match Uv::parse_metadata_output(db.system(), output) { Ok(metadata) => (Some(metadata), None), diff --git a/crates/ty_project/src/uv/metadata.rs b/crates/ty_project/src/uv/metadata.rs index 7fac020f3dd7e..3eb3bbc8ffa91 100644 --- a/crates/ty_project/src/uv/metadata.rs +++ b/crates/ty_project/src/uv/metadata.rs @@ -2,6 +2,7 @@ use std::collections::BTreeMap; use std::path::PathBuf; use char_str::CharStr; +use pep440_rs::Version; use ruff_db::system::{System, SystemPath, SystemPathBuf}; use serde::Deserialize; use thiserror::Error; @@ -82,6 +83,13 @@ pub(crate) struct WorkspaceMember { #[derive(Debug, Error)] pub(crate) enum UvMetadataError { + #[error("uv {version} is too old; upgrade `{executable}` to uv {minimum_version} or newer")] + UnsupportedVersion { + executable: String, + version: Version, + minimum_version: Version, + }, + #[error("Failed to invoke `uv workspace metadata`: {0}")] Invocation(#[source] std::io::Error), diff --git a/crates/ty_project/src/uv/service.rs b/crates/ty_project/src/uv/service.rs index b9e10cc8f1156..25de0152b472f 100644 --- a/crates/ty_project/src/uv/service.rs +++ b/crates/ty_project/src/uv/service.rs @@ -7,7 +7,7 @@ use ruff_db::files::File; use ruff_db::system::{CommandExecutor, System, SystemPathBuf}; use super::command::unsupported_command_execution; -use super::{MetadataTarget, ScriptEnvironmentCacheKey, Uv, uv_executable_error}; +use super::{MetadataTarget, ScriptEnvironmentCacheKey, Uv, UvMetadataError, uv_executable_error}; use crate::UvSyncProgress; /// Runs workspace and standalone-script metadata requests with uv. @@ -71,7 +71,7 @@ impl UvMetadataService { Err(error) => { self.publish_result(UvMetadataResult { task, - output: Some(Err(error)), + output: Some(Err(UvMetadataError::Invocation(error))), progress, }); return; @@ -97,7 +97,7 @@ impl UvMetadataService { let job = error.into_inner(); self.publish_result(UvMetadataResult { task: job.task, - output: Some(Err(worker_disconnected())), + output: Some(Err(UvMetadataError::Invocation(worker_disconnected()))), progress: job.progress, }); } @@ -211,7 +211,7 @@ impl UvSyncTask { pub(crate) struct UvMetadataResult { pub(crate) task: UvSyncTask, /// `None` if the worker skipped this request because it was cancelled before execution. - pub(crate) output: Option>, + pub(crate) output: Option>, pub(crate) progress: Option>, } From c4e2d2f052f3042e2c6d28a1d3b7605e5e71bfc8 Mon Sep 17 00:00:00 2001 From: David Peter Date: Mon, 28 Sep 2026 12:19:00 +0200 Subject: [PATCH 2/4] [ty] Keep the uv version diagnostic change minimal --- crates/ty/tests/cli/scripts.rs | 47 ------- crates/ty_project/src/uv/command.rs | 177 ++------------------------- crates/ty_project/src/uv/metadata.rs | 3 +- 3 files changed, 8 insertions(+), 219 deletions(-) diff --git a/crates/ty/tests/cli/scripts.rs b/crates/ty/tests/cli/scripts.rs index 5a39a8675af83..1e3a7ca5e7702 100644 --- a/crates/ty/tests/cli/scripts.rs +++ b/crates/ty/tests/cli/scripts.rs @@ -1,6 +1,3 @@ -#[cfg(unix)] -use std::{fs, os::unix::fs::PermissionsExt}; - use insta_cmd::assert_cmd_snapshot; use ty_static::EnvVars; @@ -1355,50 +1352,6 @@ fn unavailable_uv_reports_metadata_error() -> anyhow::Result<()> { Ok(()) } -#[test] -#[cfg(unix)] -fn old_uv_reports_upgrade() -> anyhow::Result<()> { - let case = CliTest::with_files([ - ("script.py", "# /// script\n# dependencies = []\n# ///\n"), - ( - "uv", - "#!/bin/sh\nif [ \"$1\" = '--version' ]; then echo 'uv 0.12.2'; fi\n", - ), - ])?; - let uv = case.root().join("uv"); - fs::set_permissions(&uv, fs::Permissions::from_mode(0o755))?; - - assert_cmd_snapshot!( - case.command().arg("script.py").env(EnvVars::TY_UV, "scripts").env(EnvVars::UV, &uv), - @" - success: false - exit_code: 1 - ----- stdout ----- - error[uv-metadata]: uv 0.12.2 is too old; upgrade `/uv` to uv 0.12.3 or newer - --> script.py:1:1 - - Found 1 diagnostic - - ----- stderr ----- - " - ); - assert_cmd_snapshot!( - case.command().arg("script.py").args(["--output-format", "concise"]) - .env(EnvVars::TY_UV, "scripts").env(EnvVars::UV, &uv), - @" - success: false - exit_code: 1 - ----- stdout ----- - script.py:1:1: error[uv-metadata] uv 0.12.2 is too old; upgrade `/uv` to uv 0.12.3 or newer - Found 1 diagnostic - - ----- stderr ----- - " - ); - - Ok(()) -} - #[test] fn ordinary_files_do_not_initialize_scripts() -> anyhow::Result<()> { let case = CliTest::with_files([ diff --git a/crates/ty_project/src/uv/command.rs b/crates/ty_project/src/uv/command.rs index e425476c7d8e5..7c3e2a867ee63 100644 --- a/crates/ty_project/src/uv/command.rs +++ b/crates/ty_project/src/uv/command.rs @@ -87,15 +87,12 @@ impl Uv { // Only probe the version when metadata is unavailable, so successful calls stay cheap. if (!output.status.success() || output.stdout.is_empty()) && let Some(version) = self.version(executor, target) + && version < Version::new([0, 12, 3]) { - let minimum_version = Version::new([0, 12, 3]); - if version < minimum_version { - return Err(UvMetadataError::UnsupportedVersion { - executable: self.executable.clone(), - version, - minimum_version, - }); - } + return Err(UvMetadataError::UnsupportedVersion { + executable: self.executable.clone(), + version, + }); } Ok(output) @@ -177,47 +174,10 @@ pub(super) fn unsupported_command_execution() -> std::io::Error { #[cfg(test)] mod tests { - use std::collections::VecDeque; - #[cfg(unix)] - use std::os::unix::process::ExitStatusExt; - #[cfg(windows)] - use std::os::windows::process::ExitStatusExt; - use std::process::{ExitStatus, Output}; - use std::sync::Arc; - - use parking_lot::Mutex; - use ruff_db::system::{Command, CommandExecutor, SystemPath, TestSystem}; + use ruff_db::system::TestSystem; use ty_static::EnvVars; - use super::{MetadataTarget, Uv, UvMetadataError}; - - #[derive(Clone, Default)] - struct MockExecutor { - outputs: Arc>>>, - commands: Arc>>, - } - - impl CommandExecutor for MockExecutor { - fn execute(&self, command: Command) -> std::io::Result { - self.commands.lock().push(command); - self.outputs - .lock() - .pop_front() - .unwrap_or_else(|| Err(std::io::Error::other("unexpected command"))) - } - - fn dyn_clone(&self) -> Box { - Box::new(self.clone()) - } - } - - fn output(success: bool, stdout: &str, stderr: &str) -> Output { - Output { - status: ExitStatus::from_raw((!success).into()), - stdout: stdout.as_bytes().to_vec(), - stderr: stderr.as_bytes().to_vec(), - } - } + use super::Uv; #[test] fn explicit_uv_override_skips_path_lookup() -> anyhow::Result<()> { @@ -230,127 +190,4 @@ mod tests { Ok(()) } - - #[test] - fn successful_metadata_does_not_probe_version() -> anyhow::Result<()> { - let system = TestSystem::default(); - let root = SystemPath::new(if cfg!(windows) { "C:/app" } else { "/app" }); - system.memory_file_system().create_directory_all(root)?; - let metadata = serde_json::json!({ - "schema": {"version": "preview"}, - "workspace_root": root.as_str(), - }); - let executor = MockExecutor::default(); - executor - .outputs - .lock() - .push_back(Ok(output(true, &metadata.to_string(), ""))); - let uv = Uv { - executable: "custom-uv".into(), - }; - - let result = uv.execute(&executor, &MetadataTarget::Workspace(root)); - assert_eq!( - Uv::parse_metadata_output(&system, result)?.workspace_root(), - root - ); - assert_eq!(executor.commands.lock().len(), 1); - - Ok(()) - } - - #[test] - fn old_uv_reports_upgrade() { - // Old uv can either reject the metadata command or succeed with no JSON due to --quiet. - for metadata in [ - output(false, "", "unrecognized subcommand"), - output(true, "", ""), - ] { - let executor = MockExecutor::default(); - executor.outputs.lock().extend([ - Ok(metadata), - Ok(output(true, "uv 0.12.2 (abcdef 2026-08-05)\n", "")), - ]); - let uv = Uv { - executable: "custom-uv".into(), - }; - let result = uv.execute( - &executor, - &MetadataTarget::Script { - path: SystemPath::new("/app/script.py"), - python: None, - }, - ); - - let error = result.unwrap_err(); - assert_eq!( - error.to_string(), - "uv 0.12.2 is too old; upgrade `custom-uv` to uv 0.12.3 or newer" - ); - let commands = executor.commands.lock(); - assert_eq!(commands.len(), 2); - assert!( - commands - .iter() - .all(|command| command.get_executable() == "custom-uv") - ); - assert_eq!(commands[1].get_args(), ["--version"]); - assert_eq!(commands[1].get_current_dir(), commands[0].get_current_dir()); - } - } - - #[test] - fn preserves_errors_without_an_unsupported_version() { - for version in [ - Ok(output(true, "uv 0.12.3\n", "")), - Ok(output(true, "uv 0.12.19 (abcdef 2026-09-24)\n", "")), - Ok(output(true, "uv 1.0.0\n", "")), - Ok(output(true, "unknown version\n", "")), - Ok(output(false, "uv 0.12.2\n", "version failed")), - Err(std::io::Error::other("version unavailable")), - ] { - let executor = MockExecutor::default(); - let metadata = output(false, "", "dependency could not be resolved"); - let status = metadata.status; - executor.outputs.lock().extend([Ok(metadata), version]); - let uv = Uv { - executable: "custom-uv".into(), - }; - let result = uv.execute( - &executor, - &MetadataTarget::Workspace(SystemPath::new("/app")), - ); - let error = Uv::parse_metadata_output(&TestSystem::default(), result).unwrap_err(); - assert!( - matches!(error, UvMetadataError::CommandFailed { status: actual, stderr } - if actual == status && stderr == "dependency could not be resolved") - ); - } - } - - #[test] - fn preserves_invalid_metadata_from_supported_uv() { - for stdout in ["", "invalid JSON"] { - let executor = MockExecutor::default(); - executor.outputs.lock().extend([ - Ok(output(true, stdout, "")), - Ok(output(true, "uv 0.12.3\n", "")), - ]); - let uv = Uv { - executable: "custom-uv".into(), - }; - let result = uv.execute( - &executor, - &MetadataTarget::Workspace(SystemPath::new("/app")), - ); - assert!(matches!( - Uv::parse_metadata_output(&TestSystem::default(), result), - Err(UvMetadataError::InvalidMetadata(_)) - )); - assert_eq!( - executor.commands.lock().len(), - if stdout.is_empty() { 2 } else { 1 } - ); - } - } } diff --git a/crates/ty_project/src/uv/metadata.rs b/crates/ty_project/src/uv/metadata.rs index 3eb3bbc8ffa91..59fc5b0bb969c 100644 --- a/crates/ty_project/src/uv/metadata.rs +++ b/crates/ty_project/src/uv/metadata.rs @@ -83,11 +83,10 @@ pub(crate) struct WorkspaceMember { #[derive(Debug, Error)] pub(crate) enum UvMetadataError { - #[error("uv {version} is too old; upgrade `{executable}` to uv {minimum_version} or newer")] + #[error("uv {version} is too old; upgrade `{executable}` to uv 0.12.3 or newer")] UnsupportedVersion { executable: String, version: Version, - minimum_version: Version, }, #[error("Failed to invoke `uv workspace metadata`: {0}")] From b67f5944ebd591b886941053692850fc50d27128 Mon Sep 17 00:00:00 2001 From: David Peter Date: Mon, 28 Sep 2026 12:28:58 +0200 Subject: [PATCH 3/4] [ty] Reuse the uv directory and minimum version --- crates/ty_project/src/uv/command.rs | 24 ++++++++++++------------ crates/ty_project/src/uv/metadata.rs | 4 +++- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/crates/ty_project/src/uv/command.rs b/crates/ty_project/src/uv/command.rs index 7c3e2a867ee63..587c81f2d085a 100644 --- a/crates/ty_project/src/uv/command.rs +++ b/crates/ty_project/src/uv/command.rs @@ -8,6 +8,8 @@ use ty_static::EnvVars; use super::{UvMetadata, UvMetadataError}; +pub(super) const MINIMUM_UV_VERSION: [u64; 3] = [0, 12, 3]; + #[derive(Clone)] pub(crate) struct Uv { executable: String, @@ -50,21 +52,23 @@ impl Uv { let mut command = Command::new(self.executable.as_str()); command.args(["workspace", "metadata", "--quiet"]); - match target { + let directory = match target { MetadataTarget::Workspace(path) => { // Use the environment selected by `uv check` without synchronizing it. // Let uv apply its configured lockfile policy. - command.arg("--active").current_dir(path); + command.arg("--active"); + Some(*path) } MetadataTarget::Script { path, python } => { command.args(["--sync", "--script", path.as_str()]); if let Some(python) = python { command.args(["--python", python.as_str()]); } - if let Some(parent) = path.parent() { - command.current_dir(parent); - } + path.parent() } + }; + if let Some(directory) = directory { + command.current_dir(directory); } tracing::debug!( @@ -86,8 +90,8 @@ impl Uv { // Before uv 0.12.3, `--quiet` suppresses the metadata JSON even on success. // Only probe the version when metadata is unavailable, so successful calls stay cheap. if (!output.status.success() || output.stdout.is_empty()) - && let Some(version) = self.version(executor, target) - && version < Version::new([0, 12, 3]) + && let Some(version) = self.version(executor, directory) + && version < Version::new(MINIMUM_UV_VERSION) { return Err(UvMetadataError::UnsupportedVersion { executable: self.executable.clone(), @@ -101,14 +105,10 @@ impl Uv { fn version( &self, executor: &dyn CommandExecutor, - target: &MetadataTarget<'_>, + directory: Option<&SystemPath>, ) -> Option { let mut command = Command::new(self.executable.as_str()); command.arg("--version"); - let directory = match target { - MetadataTarget::Workspace(path) => Some(*path), - MetadataTarget::Script { path, .. } => path.parent(), - }; if let Some(directory) = directory { command.current_dir(directory); } diff --git a/crates/ty_project/src/uv/metadata.rs b/crates/ty_project/src/uv/metadata.rs index 59fc5b0bb969c..3a51da8ec6711 100644 --- a/crates/ty_project/src/uv/metadata.rs +++ b/crates/ty_project/src/uv/metadata.rs @@ -7,6 +7,8 @@ use ruff_db::system::{System, SystemPath, SystemPathBuf}; use serde::Deserialize; use thiserror::Error; +use super::command::MINIMUM_UV_VERSION; + mod dependencies; mod string_interner; @@ -83,7 +85,7 @@ pub(crate) struct WorkspaceMember { #[derive(Debug, Error)] pub(crate) enum UvMetadataError { - #[error("uv {version} is too old; upgrade `{executable}` to uv 0.12.3 or newer")] + #[error("uv {version} is too old; upgrade `{executable}` to uv {minimum_version} or newer", minimum_version = Version::new(MINIMUM_UV_VERSION))] UnsupportedVersion { executable: String, version: Version, From b30371ac4cf5536cf7c4545db1c6f718ca2a091a Mon Sep 17 00:00:00 2001 From: David Peter Date: Mon, 28 Sep 2026 12:35:14 +0200 Subject: [PATCH 4/4] [ty] Restore uv execution docstring --- crates/ty_project/src/uv/command.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/ty_project/src/uv/command.rs b/crates/ty_project/src/uv/command.rs index 587c81f2d085a..2f68ed63a0a70 100644 --- a/crates/ty_project/src/uv/command.rs +++ b/crates/ty_project/src/uv/command.rs @@ -39,7 +39,7 @@ impl Uv { Self::parse_metadata_output(system, output) } - /// Executes `uv workspace metadata`, checking for an unsupported uv version on failure. + /// Executes `uv workspace metadata` without interpreting its output. /// /// This operation only requires a detached command executor, so it can run on a background /// worker. @@ -88,7 +88,6 @@ impl Uv { let output = output.map_err(UvMetadataError::Invocation)?; // Before uv 0.12.3, `--quiet` suppresses the metadata JSON even on success. - // Only probe the version when metadata is unavailable, so successful calls stay cheap. if (!output.status.success() || output.stdout.is_empty()) && let Some(version) = self.version(executor, directory) && version < Version::new(MINIMUM_UV_VERSION)