From e10881d49c8649a66f301af9052fd2bb17eb68d4 Mon Sep 17 00:00:00 2001 From: konsti Date: Fri, 13 Jun 2025 11:57:45 +0200 Subject: [PATCH 001/185] Add tests for IO Error retries (#13627) Often, HTTP requests don't fail due to server errors, but from spurious network errors such as connection resets. reqwest surfaces these as `io::Error`, and we have to handle their retrying separately. Companion PR: https://github.com/LukeMathWalker/wiremock-rs/pull/159 --- Cargo.lock | 3 +- Cargo.toml | 2 +- crates/uv/tests/it/network.rs | 193 ++++++++++++++++++++++++++++------ 3 files changed, 162 insertions(+), 36 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4dd29fe69..565b9b481 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6740,8 +6740,7 @@ checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" [[package]] name = "wiremock" version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "101681b74cd87b5899e87bcf5a64e83334dd313fcd3053ea72e6dba18928e301" +source = "git+https://github.com/astral-sh/wiremock-rs?rev=b79b69f62521df9f83a54e866432397562eae789#b79b69f62521df9f83a54e866432397562eae789" dependencies = [ "assert-json-diff", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index 0f1d02b47..55bbde3e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -189,7 +189,7 @@ windows-core = { version = "0.59.0" } windows-registry = { version = "0.5.0" } windows-result = { version = "0.3.0" } windows-sys = { version = "0.59.0", features = ["Win32_Foundation", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Ioctl", "Win32_System_IO", "Win32_System_Registry"] } -wiremock = { version = "0.6.2" } +wiremock = { git = "https://github.com/astral-sh/wiremock-rs", rev = "b79b69f62521df9f83a54e866432397562eae789" } xz2 = { version = "0.1.7" } zip = { version = "2.2.3", default-features = false, features = ["deflate", "zstd", "bzip2", "lzma", "xz"] } diff --git a/crates/uv/tests/it/network.rs b/crates/uv/tests/it/network.rs index fba24afe1..8edbc63a2 100644 --- a/crates/uv/tests/it/network.rs +++ b/crates/uv/tests/it/network.rs @@ -1,6 +1,6 @@ -use std::env; +use std::{env, io}; -use assert_fs::fixture::{FileWriteStr, PathChild}; +use assert_fs::fixture::{ChildPath, FileWriteStr, PathChild}; use http::StatusCode; use serde_json::json; use wiremock::matchers::method; @@ -8,24 +8,47 @@ use wiremock::{Mock, MockServer, ResponseTemplate}; use crate::common::{TestContext, uv_snapshot}; -/// Check the simple index error message when the server returns HTTP status 500, a retryable error. -#[tokio::test] -async fn simple_http_500() { - let context = TestContext::new("3.12"); +fn connection_reset(_request: &wiremock::Request) -> io::Error { + io::Error::new(io::ErrorKind::ConnectionReset, "Connection reset by peer") +} +/// Answers with a retryable HTTP status 500. +async fn http_error_server() -> (MockServer, String) { let server = MockServer::start().await; Mock::given(method("GET")) .respond_with(ResponseTemplate::new(StatusCode::INTERNAL_SERVER_ERROR)) .mount(&server) .await; + let mock_server_uri = server.uri(); + (server, mock_server_uri) +} + +/// Answers with a retryable connection reset IO error. +async fn io_error_server() -> (MockServer, String) { + let server = MockServer::start().await; + Mock::given(method("GET")) + .respond_with_err(connection_reset) + .mount(&server) + .await; + + let mock_server_uri = server.uri(); + (server, mock_server_uri) +} + +/// Check the simple index error message when the server returns HTTP status 500, a retryable error. +#[tokio::test] +async fn simple_http_500() { + let context = TestContext::new("3.12"); + + let (_server_drop_guard, mock_server_uri) = http_error_server().await; let filters = vec![(mock_server_uri.as_str(), "[SERVER]")]; uv_snapshot!(filters, context .pip_install() .arg("tqdm") .arg("--index-url") - .arg(server.uri()), @r" + .arg(&mock_server_uri), @r" success: false exit_code: 2 ----- stdout ----- @@ -36,17 +59,38 @@ async fn simple_http_500() { "); } +/// Check the simple index error message when the server returns a retryable IO error. +#[tokio::test] +async fn simple_io_err() { + let context = TestContext::new("3.12"); + + let (_server_drop_guard, mock_server_uri) = io_error_server().await; + + let filters = vec![(mock_server_uri.as_str(), "[SERVER]")]; + uv_snapshot!(filters, context + .pip_install() + .arg("tqdm") + .arg("--index-url") + .arg(&mock_server_uri), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Failed to fetch: `[SERVER]/tqdm/` + Caused by: Request failed after 3 retries + Caused by: error sending request for url ([SERVER]/tqdm/) + Caused by: client error (SendRequest) + Caused by: connection closed before message completed + "); +} + /// Check the find links error message when the server returns HTTP status 500, a retryable error. #[tokio::test] async fn find_links_http_500() { let context = TestContext::new("3.12"); - let server = MockServer::start().await; - Mock::given(method("GET")) - .respond_with(ResponseTemplate::new(StatusCode::INTERNAL_SERVER_ERROR)) - .mount(&server) - .await; - let mock_server_uri = server.uri(); + let (_server_drop_guard, mock_server_uri) = http_error_server().await; let filters = vec![(mock_server_uri.as_str(), "[SERVER]")]; uv_snapshot!(filters, context @@ -54,7 +98,7 @@ async fn find_links_http_500() { .arg("tqdm") .arg("--no-index") .arg("--find-links") - .arg(server.uri()), @r" + .arg(&mock_server_uri), @r" success: false exit_code: 2 ----- stdout ----- @@ -66,18 +110,41 @@ async fn find_links_http_500() { "); } +/// Check the find links error message when the server returns a retryable IO error. +#[tokio::test] +async fn find_links_io_error() { + let context = TestContext::new("3.12"); + + let (_server_drop_guard, mock_server_uri) = io_error_server().await; + + let filters = vec![(mock_server_uri.as_str(), "[SERVER]")]; + uv_snapshot!(filters, context + .pip_install() + .arg("tqdm") + .arg("--no-index") + .arg("--find-links") + .arg(&mock_server_uri), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Failed to read `--find-links` URL: [SERVER]/ + Caused by: Failed to fetch: `[SERVER]/` + Caused by: Request failed after 3 retries + Caused by: error sending request for url ([SERVER]/) + Caused by: client error (SendRequest) + Caused by: connection closed before message completed + "); +} + /// Check the direct package URL error message when the server returns HTTP status 500, a retryable /// error. #[tokio::test] async fn direct_url_http_500() { let context = TestContext::new("3.12"); - let server = MockServer::start().await; - Mock::given(method("GET")) - .respond_with(ResponseTemplate::new(StatusCode::INTERNAL_SERVER_ERROR)) - .mount(&server) - .await; - let mock_server_uri = server.uri(); + let (_server_drop_guard, mock_server_uri) = http_error_server().await; let tqdm_url = format!( "{mock_server_uri}/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl" @@ -97,22 +164,35 @@ async fn direct_url_http_500() { "); } -/// Check the Python install error message when the server returns HTTP status 500, a retryable -/// error. +/// Check the direct package URL error message when the server returns a retryable IO error. #[tokio::test] -async fn python_install_http_500() { - let context = TestContext::new("3.12") - .with_filtered_python_keys() - .with_filtered_exe_suffix() - .with_managed_python_dirs(); +async fn direct_url_io_error() { + let context = TestContext::new("3.12"); - let server = MockServer::start().await; - Mock::given(method("GET")) - .respond_with(ResponseTemplate::new(StatusCode::INTERNAL_SERVER_ERROR)) - .mount(&server) - .await; - let mock_server_uri = server.uri(); + let (_server_drop_guard, mock_server_uri) = io_error_server().await; + let tqdm_url = format!( + "{mock_server_uri}/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl" + ); + let filters = vec![(mock_server_uri.as_str(), "[SERVER]")]; + uv_snapshot!(filters, context + .pip_install() + .arg(format!("tqdm @ {tqdm_url}")), @r" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × Failed to download `tqdm @ [SERVER]/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl` + ├─▶ Failed to fetch: `[SERVER]/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl` + ├─▶ Request failed after 3 retries + ├─▶ error sending request for url ([SERVER]/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl) + ├─▶ client error (SendRequest) + ╰─▶ connection closed before message completed + "); +} + +fn write_python_downloads_json(context: &TestContext, mock_server_uri: &String) -> ChildPath { let python_downloads_json = context.temp_dir.child("python_downloads.json"); let interpreter = json!({ "cpython-3.10.0-darwin-aarch64-none": { @@ -135,6 +215,21 @@ async fn python_install_http_500() { python_downloads_json .write_str(&serde_json::to_string(&interpreter).unwrap()) .unwrap(); + python_downloads_json +} + +/// Check the Python install error message when the server returns HTTP status 500, a retryable +/// error. +#[tokio::test] +async fn python_install_http_500() { + let context = TestContext::new("3.12") + .with_filtered_python_keys() + .with_filtered_exe_suffix() + .with_managed_python_dirs(); + + let (_server_drop_guard, mock_server_uri) = http_error_server().await; + + let python_downloads_json = write_python_downloads_json(&context, &mock_server_uri); let filters = vec![(mock_server_uri.as_str(), "[SERVER]")]; uv_snapshot!(filters, context @@ -152,3 +247,35 @@ async fn python_install_http_500() { Caused by: HTTP status server error (500 Internal Server Error) for url ([SERVER]/astral-sh/python-build-standalone/releases/download/20211017/cpython-3.10.0-aarch64-apple-darwin-pgo%2Blto-20211017T1616.tar.zst) "); } + +/// Check the Python install error message when the server returns a retryable IO error. +#[tokio::test] +async fn python_install_io_error() { + let context = TestContext::new("3.12") + .with_filtered_python_keys() + .with_filtered_exe_suffix() + .with_managed_python_dirs(); + + let (_server_drop_guard, mock_server_uri) = io_error_server().await; + + let python_downloads_json = write_python_downloads_json(&context, &mock_server_uri); + + let filters = vec![(mock_server_uri.as_str(), "[SERVER]")]; + uv_snapshot!(filters, context + .python_install() + .arg("cpython-3.10.0-darwin-aarch64-none") + .arg("--python-downloads-json-url") + .arg(python_downloads_json.path()), @r" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + error: Failed to install cpython-3.10.0-macos-aarch64-none + Caused by: Failed to download [SERVER]/astral-sh/python-build-standalone/releases/download/20211017/cpython-3.10.0-aarch64-apple-darwin-pgo%2Blto-20211017T1616.tar.zst + Caused by: Request failed after 3 retries + Caused by: error sending request for url ([SERVER]/astral-sh/python-build-standalone/releases/download/20211017/cpython-3.10.0-aarch64-apple-darwin-pgo%2Blto-20211017T1616.tar.zst) + Caused by: client error (SendRequest) + Caused by: connection closed before message completed + "); +} From b95e66019de143633b6ce304974b80815f1a1b9e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 13 Jun 2025 13:37:11 +0200 Subject: [PATCH 002/185] Update Rust crate hyper-util to v0.1.14 (#13912) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [hyper-util](https://hyper.rs) ([source](https://redirect.github.com/hyperium/hyper-util)) | dev-dependencies | patch | `0.1.12` -> `0.1.14` | --- > [!WARNING] > Some dependencies could not be looked up. Check the Dependency Dashboard for more information. --- ### Release Notes
hyperium/hyper-util (hyper-util) ### [`v0.1.14`](https://redirect.github.com/hyperium/hyper-util/blob/HEAD/CHANGELOG.md#0114-2025-06-04) [Compare Source](https://redirect.github.com/hyperium/hyper-util/compare/v0.1.13...v0.1.14) - Fix `HttpConnector` to defer address family order to resolver sort order. - Fix `proxy::Matcher` to find HTTPS system proxies on Windows. ### [`v0.1.13`](https://redirect.github.com/hyperium/hyper-util/blob/HEAD/CHANGELOG.md#0113-2025-05-27) [Compare Source](https://redirect.github.com/hyperium/hyper-util/compare/v0.1.12...v0.1.13) - Fix `HttpConnector` to always prefer IPv6 addresses first, if happy eyeballs is enabled. - Fix `legacy::Client` to return better errors if available on the connection.
--- ### Configuration 📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/astral-sh/uv). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 565b9b481..0a862acf8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1680,12 +1680,13 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.12" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9f1e950e0d9d1d3c47184416723cf29c0d1f93bd8cccf37e4beb6b44f31710" +checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb" dependencies = [ "bytes", "futures-channel", + "futures-core", "futures-util", "http", "http-body", From 881e17600f5604b36012c5a97682e3ccf8a267c2 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Fri, 13 Jun 2025 08:50:44 -0500 Subject: [PATCH 003/185] Filter managed Python distributions by platform before querying when included in request (#13936) In the case where we have platform information in a Python request, we should filter managed Python distributions by it prior to querying them. Closes https://github.com/astral-sh/uv/issues/13935 --------- Co-authored-by: Aria Desires --- crates/uv-python/src/discovery.rs | 64 +++++++++++++++++++++++-------- crates/uv-python/src/downloads.rs | 57 +++++++++++++++++++++++++++ crates/uv-python/src/lib.rs | 1 + 3 files changed, 105 insertions(+), 17 deletions(-) diff --git a/crates/uv-python/src/discovery.rs b/crates/uv-python/src/discovery.rs index 274cb51d4..0be8d17d1 100644 --- a/crates/uv-python/src/discovery.rs +++ b/crates/uv-python/src/discovery.rs @@ -20,7 +20,7 @@ use uv_pep440::{ use uv_static::EnvVars; use uv_warnings::warn_user_once; -use crate::downloads::PythonDownloadRequest; +use crate::downloads::{PlatformRequest, PythonDownloadRequest}; use crate::implementation::ImplementationName; use crate::installation::PythonInstallation; use crate::interpreter::Error as InterpreterError; @@ -312,6 +312,7 @@ fn python_executables_from_virtual_environments<'a>() fn python_executables_from_installed<'a>( version: &'a VersionRequest, implementation: Option<&'a ImplementationName>, + platform: PlatformRequest, preference: PythonPreference, ) -> Box> + 'a> { let from_managed_installations = iter::once_with(move || { @@ -323,16 +324,19 @@ fn python_executables_from_installed<'a>( installed_installations.root().user_display() ); let installations = installed_installations.find_matching_current_platform()?; - // Check that the Python version satisfies the request to avoid unnecessary interpreter queries later + // Check that the Python version and platform satisfy the request to avoid unnecessary interpreter queries later Ok(installations .into_iter() .filter(move |installation| { - if version.matches_version(&installation.version()) { - true - } else { - debug!("Skipping incompatible managed installation `{installation}`"); - false + if !version.matches_version(&installation.version()) { + debug!("Skipping managed installation `{installation}`: does not satisfy `{version}`"); + return false; } + if !platform.matches(installation.key()) { + debug!("Skipping managed installation `{installation}`: does not satisfy `{platform}`"); + return false; + } + true }) .inspect(|installation| debug!("Found managed installation `{installation}`")) .map(|installation| (PythonSource::Managed, installation.executable(false)))) @@ -415,15 +419,17 @@ fn python_executables_from_installed<'a>( /// Lazily iterate over all discoverable Python executables. /// -/// Note that Python executables may be excluded by the given [`EnvironmentPreference`] and -/// [`PythonPreference`]. However, these filters are only applied for performance. We cannot -/// guarantee that the [`EnvironmentPreference`] is satisfied until we query the interpreter. +/// Note that Python executables may be excluded by the given [`EnvironmentPreference`], +/// [`PythonPreference`], and [`PlatformRequest`]. However, these filters are only applied for +/// performance. We cannot guarantee that the all requests or preferences are satisfied until we +/// query the interpreter. /// /// See [`python_executables_from_installed`] and [`python_executables_from_virtual_environments`] /// for more information on discovery. fn python_executables<'a>( version: &'a VersionRequest, implementation: Option<&'a ImplementationName>, + platform: PlatformRequest, environments: EnvironmentPreference, preference: PythonPreference, ) -> Box> + 'a> { @@ -445,7 +451,8 @@ fn python_executables<'a>( .flatten(); let from_virtual_environments = python_executables_from_virtual_environments(); - let from_installed = python_executables_from_installed(version, implementation, preference); + let from_installed = + python_executables_from_installed(version, implementation, platform, preference); // Limit the search to the relevant environment preference; this avoids unnecessary work like // traversal of the file system. Subsequent filtering should be done by the caller with @@ -630,12 +637,17 @@ fn find_all_minor( /// Lazily iterate over all discoverable Python interpreters. /// -/// Note interpreters may be excluded by the given [`EnvironmentPreference`] and [`PythonPreference`]. +/// Note interpreters may be excluded by the given [`EnvironmentPreference`], [`PythonPreference`], +/// [`VersionRequest`], or [`PlatformRequest`]. +/// +/// The [`PlatformRequest`] is currently only applied to managed Python installations before querying +/// the interpreter. The caller is responsible for ensuring it is applied otherwise. /// /// See [`python_executables`] for more information on discovery. fn python_interpreters<'a>( version: &'a VersionRequest, implementation: Option<&'a ImplementationName>, + platform: PlatformRequest, environments: EnvironmentPreference, preference: PythonPreference, cache: &'a Cache, @@ -644,7 +656,7 @@ fn python_interpreters<'a>( // Perform filtering on the discovered executables based on their source. This avoids // unnecessary interpreter queries, which are generally expensive. We'll filter again // with `interpreter_satisfies_environment_preference` after querying. - python_executables(version, implementation, environments, preference).filter_ok( + python_executables(version, implementation, platform, environments, preference).filter_ok( move |(source, path)| { source_satisfies_environment_preference(*source, path, environments) }, @@ -971,14 +983,22 @@ pub fn find_python_installations<'a>( } PythonRequest::Any => Box::new({ debug!("Searching for any Python interpreter in {sources}"); - python_interpreters(&VersionRequest::Any, None, environments, preference, cache) - .map_ok(|tuple| Ok(PythonInstallation::from_tuple(tuple))) + python_interpreters( + &VersionRequest::Any, + None, + PlatformRequest::default(), + environments, + preference, + cache, + ) + .map_ok(|tuple| Ok(PythonInstallation::from_tuple(tuple))) }), PythonRequest::Default => Box::new({ debug!("Searching for default Python interpreter in {sources}"); python_interpreters( &VersionRequest::Default, None, + PlatformRequest::default(), environments, preference, cache, @@ -991,8 +1011,15 @@ pub fn find_python_installations<'a>( } Box::new({ debug!("Searching for {request} in {sources}"); - python_interpreters(version, None, environments, preference, cache) - .map_ok(|tuple| Ok(PythonInstallation::from_tuple(tuple))) + python_interpreters( + version, + None, + PlatformRequest::default(), + environments, + preference, + cache, + ) + .map_ok(|tuple| Ok(PythonInstallation::from_tuple(tuple))) }) } PythonRequest::Implementation(implementation) => Box::new({ @@ -1000,6 +1027,7 @@ pub fn find_python_installations<'a>( python_interpreters( &VersionRequest::Default, Some(implementation), + PlatformRequest::default(), environments, preference, cache, @@ -1020,6 +1048,7 @@ pub fn find_python_installations<'a>( python_interpreters( version, Some(implementation), + PlatformRequest::default(), environments, preference, cache, @@ -1043,6 +1072,7 @@ pub fn find_python_installations<'a>( python_interpreters( request.version().unwrap_or(&VersionRequest::Default), request.implementation(), + request.platform(), environments, preference, cache, diff --git a/crates/uv-python/src/downloads.rs b/crates/uv-python/src/downloads.rs index 9b7fc2825..0b7517960 100644 --- a/crates/uv-python/src/downloads.rs +++ b/crates/uv-python/src/downloads.rs @@ -131,6 +131,54 @@ pub enum ArchRequest { Environment(Arch), } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct PlatformRequest { + pub(crate) os: Option, + pub(crate) arch: Option, + pub(crate) libc: Option, +} + +impl PlatformRequest { + /// Check if this platform request is satisfied by an installation key. + pub fn matches(&self, key: &PythonInstallationKey) -> bool { + if let Some(os) = self.os { + if key.os != os { + return false; + } + } + + if let Some(arch) = self.arch { + if !arch.satisfied_by(key.arch) { + return false; + } + } + + if let Some(libc) = self.libc { + if key.libc != libc { + return false; + } + } + + true + } +} + +impl Display for PlatformRequest { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut parts = Vec::new(); + if let Some(os) = &self.os { + parts.push(os.to_string()); + } + if let Some(arch) = &self.arch { + parts.push(arch.to_string()); + } + if let Some(libc) = &self.libc { + parts.push(libc.to_string()); + } + write!(f, "{}", parts.join("-")) + } +} + impl Display for ArchRequest { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -412,6 +460,15 @@ impl PythonDownloadRequest { } true } + + /// Extract the platform components of this request. + pub fn platform(&self) -> PlatformRequest { + PlatformRequest { + os: self.os, + arch: self.arch, + libc: self.libc, + } + } } impl From<&ManagedPythonInstallation> for PythonDownloadRequest { diff --git a/crates/uv-python/src/lib.rs b/crates/uv-python/src/lib.rs index 024cd5cbc..0fa2d46ab 100644 --- a/crates/uv-python/src/lib.rs +++ b/crates/uv-python/src/lib.rs @@ -9,6 +9,7 @@ pub use crate::discovery::{ PythonPreference, PythonRequest, PythonSource, PythonVariant, VersionRequest, find_python_installations, }; +pub use crate::downloads::PlatformRequest; pub use crate::environment::{InvalidEnvironmentKind, PythonEnvironment}; pub use crate::implementation::ImplementationName; pub use crate::installation::{PythonInstallation, PythonInstallationKey}; From da2eca4e2cf0b5bb3dacbd8417c98e28de948368 Mon Sep 17 00:00:00 2001 From: konsti Date: Fri, 13 Jun 2025 15:51:17 +0200 Subject: [PATCH 004/185] Make wiremock filter a default filter (#14024) Make `127.0.0.1:` -> ``[LOCALHOST]`` a general test filter, it simplifies using wiremock and I'm not aware of any false positives. --- crates/uv/tests/it/common/mod.rs | 2 ++ crates/uv/tests/it/edit.rs | 7 +------ crates/uv/tests/it/pip_install.rs | 13 ++++--------- 3 files changed, 7 insertions(+), 15 deletions(-) diff --git a/crates/uv/tests/it/common/mod.rs b/crates/uv/tests/it/common/mod.rs index 67e1a6126..23845a8a8 100644 --- a/crates/uv/tests/it/common/mod.rs +++ b/crates/uv/tests/it/common/mod.rs @@ -650,6 +650,8 @@ impl TestContext { format!("https://raw.githubusercontent.com/astral-sh/packse/{PACKSE_VERSION}/"), "https://raw.githubusercontent.com/astral-sh/packse/PACKSE_VERSION/".to_string(), )); + // For wiremock tests + filters.push((r"127\.0\.0\.1:\d*".to_string(), "[LOCALHOST]".to_string())); Self { root: ChildPath::new(root.path()), diff --git a/crates/uv/tests/it/edit.rs b/crates/uv/tests/it/edit.rs index f96dd7b7b..10c812473 100644 --- a/crates/uv/tests/it/edit.rs +++ b/crates/uv/tests/it/edit.rs @@ -11453,11 +11453,6 @@ fn add_missing_package_on_pytorch() -> Result<()> { #[tokio::test] async fn add_unexpected_error_code() -> Result<()> { let context = TestContext::new("3.12"); - let filters = context - .filters() - .into_iter() - .chain([(r"127\.0\.0\.1(?::\d+)?", "[LOCALHOST]")]) - .collect::>(); let server = MockServer::start().await; @@ -11476,7 +11471,7 @@ async fn add_unexpected_error_code() -> Result<()> { "# })?; - uv_snapshot!(filters, context.add().arg("anyio").arg("--index").arg(server.uri()), @r" + uv_snapshot!(context.filters(), context.add().arg("anyio").arg("--index").arg(server.uri()), @r" success: false exit_code: 2 ----- stdout ----- diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index fa823cce0..815cbac1f 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -564,11 +564,6 @@ fn install_requirements_txt() -> Result<()> { #[tokio::test] async fn install_remote_requirements_txt() -> Result<()> { let context = TestContext::new("3.12"); - let filters = context - .filters() - .into_iter() - .chain([(r"127\.0\.0\.1[^\r\n]*", "[LOCALHOST]")]) - .collect::>(); let username = "user"; let password = "password"; @@ -579,17 +574,17 @@ async fn install_remote_requirements_txt() -> Result<()> { let mut requirements_url = Url::parse(&format!("{}/requirements.txt", &server_url))?; // Should fail without credentials - uv_snapshot!(filters, context.pip_install() + uv_snapshot!(context.filters(), context.pip_install() .arg("-r") .arg(requirements_url.as_str()) - .arg("--strict"), @r###" + .arg("--strict"), @r" success: false exit_code: 2 ----- stdout ----- ----- stderr ----- - error: Error while accessing remote requirements file: `http://[LOCALHOST] - "### + error: Error while accessing remote requirements file: `http://[LOCALHOST]/requirements.txt` + " ); let _ = requirements_url.set_username(username); From 49b450109b825ec8fdec7600c2a8f763496f70b7 Mon Sep 17 00:00:00 2001 From: Aria Desires Date: Fri, 13 Jun 2025 09:57:04 -0400 Subject: [PATCH 005/185] filter out riscv64 wheels before publishing to pypi (#14009) An alternative to #14006 --- .github/workflows/publish-pypi.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index de3ca4d30..84c8e44d9 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -28,6 +28,8 @@ jobs: pattern: wheels_uv-* path: wheels_uv merge-multiple: true + - name: Remove wheels unsupported by PyPI + run: rm wheels_uv/*riscv* - name: Publish to PyPI run: uv publish -v wheels_uv/* @@ -47,5 +49,7 @@ jobs: pattern: wheels_uv_build-* path: wheels_uv_build merge-multiple: true + - name: Remove wheels unsupported by PyPI + run: rm wheels_uv_build/*riscv* - name: Publish to PyPI run: uv publish -v wheels_uv_build/* From d9b76b97c20d9c0dc018d31ececf52f5d11be09f Mon Sep 17 00:00:00 2001 From: konsti Date: Fri, 13 Jun 2025 20:04:13 +0200 Subject: [PATCH 006/185] Remove unreachable pub function (#14032) Not sure how this landed on main, but rustc complained. --- crates/uv-python/src/discovery.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/uv-python/src/discovery.rs b/crates/uv-python/src/discovery.rs index 0be8d17d1..3858fd525 100644 --- a/crates/uv-python/src/discovery.rs +++ b/crates/uv-python/src/discovery.rs @@ -1185,7 +1185,7 @@ pub(crate) fn find_python_installation( /// /// See [`find_python_installation`] for more details on installation discovery. #[instrument(skip_all, fields(request))] -pub fn find_best_python_installation( +pub(crate) fn find_best_python_installation( request: &PythonRequest, environments: EnvironmentPreference, preference: PythonPreference, From 26db29caac6965359b375fb1733e591b89068ec3 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Fri, 13 Jun 2025 14:43:46 -0400 Subject: [PATCH 007/185] Update to `jiff 0.2.15` (#14035) This brings in https://github.com/BurntSushi/jiff/pull/385, which makes cold resolves about 10% faster. Or, stated differently, as fast as they were a few weeks ago before the perf regression introduced by `jiff 0.2.14`. --- Cargo.lock | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0a862acf8..ebd1c121a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1115,7 +1115,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1942,7 +1942,7 @@ checksum = "e19b23d53f35ce9f56aebc7d1bb4e6ac1e9c0db7ac85c8d1760c04379edced37" dependencies = [ "hermit-abi 0.4.0", "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1992,9 +1992,9 @@ checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "jiff" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a194df1107f33c79f4f93d02c80798520551949d59dfad22b6157048a88cca93" +checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" dependencies = [ "jiff-static", "jiff-tzdb-platform", @@ -2002,14 +2002,14 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "jiff-static" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c6e1db7ed32c6c71b759497fae34bf7933636f75a251b9e736555da426f6442" +checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" dependencies = [ "proc-macro2", "quote", @@ -2886,7 +2886,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3335,7 +3335,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3348,7 +3348,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.9.2", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3929,7 +3929,7 @@ dependencies = [ "getrandom 0.3.1", "once_cell", "rustix 1.0.7", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] From 50218409191c55587e832e649699ef5d07a611e8 Mon Sep 17 00:00:00 2001 From: Aria Desires Date: Fri, 13 Jun 2025 18:04:13 -0400 Subject: [PATCH 008/185] Add `[tool.uv.dependency-groups].mygroup.requires-python` (#13735) This allows you to specify requires-python on individual dependency-groups, with the intended usecase being "oh my dev-dependencies have a higher requires-python than my actual project". This includes a large driveby move of the RequiresPython type to uv-distribution-types to allow us to generate the appropriate markers at this point in the code. It also migrates RequiresPython from pubgrub::Range to version_ranges::Ranges, and makes several pub(crate) items pub, as it's no longer defined in uv_resolver. Fixes #11606 --- Cargo.lock | 1 + crates/uv-bench/benches/uv.rs | 6 +- .../uv-configuration/src/dependency_groups.rs | 9 + crates/uv-configuration/src/extras.rs | 8 + crates/uv-distribution-types/src/lib.rs | 2 + .../src/requires_python.rs | 33 +- .../src/metadata/requires_dist.rs | 59 +- crates/uv-resolver/src/lib.rs | 2 - .../src/lock/export/pylock_toml.rs | 6 +- crates/uv-resolver/src/lock/mod.rs | 8 +- crates/uv-resolver/src/marker.rs | 2 +- crates/uv-resolver/src/pubgrub/report.rs | 6 +- crates/uv-resolver/src/python_requirement.rs | 3 +- crates/uv-resolver/src/resolution/output.rs | 7 +- .../src/resolution/requirements_txt.rs | 10 +- .../uv-resolver/src/resolver/environment.rs | 6 +- crates/uv-resolver/src/resolver/provider.rs | 3 +- crates/uv-resolver/src/version_map.rs | 5 +- crates/uv-settings/src/settings.rs | 6 + crates/uv-workspace/Cargo.toml | 1 + crates/uv-workspace/src/dependency_groups.rs | 230 ++++++-- crates/uv-workspace/src/lib.rs | 4 +- crates/uv-workspace/src/pyproject.rs | 89 +++ crates/uv-workspace/src/workspace.rs | 90 +-- crates/uv/src/commands/build_frontend.rs | 12 +- crates/uv/src/commands/pip/compile.rs | 6 +- crates/uv/src/commands/pip/latest.rs | 4 +- crates/uv/src/commands/pip/list.rs | 6 +- crates/uv/src/commands/pip/tree.rs | 4 +- crates/uv/src/commands/project/add.rs | 88 +-- crates/uv/src/commands/project/export.rs | 13 +- crates/uv/src/commands/project/init.rs | 7 +- .../uv/src/commands/project/install_target.rs | 29 +- crates/uv/src/commands/project/lock.rs | 13 +- crates/uv/src/commands/project/lock_target.rs | 21 +- crates/uv/src/commands/project/mod.rs | 230 ++++---- crates/uv/src/commands/project/remove.rs | 22 +- crates/uv/src/commands/project/run.rs | 33 +- crates/uv/src/commands/project/sync.rs | 21 +- crates/uv/src/commands/project/tree.rs | 10 +- crates/uv/src/commands/project/version.rs | 27 +- crates/uv/src/commands/python/find.rs | 5 + crates/uv/src/commands/python/pin.rs | 9 +- crates/uv/src/commands/venv.rs | 16 +- crates/uv/src/lib.rs | 8 +- crates/uv/src/settings.rs | 16 +- crates/uv/tests/it/lock.rs | 550 ++++++++++++++++-- crates/uv/tests/it/python_find.rs | 13 +- crates/uv/tests/it/run.rs | 259 ++++++++- crates/uv/tests/it/show_settings.rs | 2 +- crates/uv/tests/it/sync.rs | 352 ++++++++++- crates/uv/tests/it/venv.rs | 184 +++++- docs/reference/settings.md | 25 + uv.schema.json | 29 + 54 files changed, 2072 insertions(+), 538 deletions(-) rename crates/{uv-resolver => uv-distribution-types}/src/requires_python.rs (97%) diff --git a/Cargo.lock b/Cargo.lock index ebd1c121a..6864b4319 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6014,6 +6014,7 @@ dependencies = [ "tracing", "uv-build-backend", "uv-cache-key", + "uv-configuration", "uv-distribution-types", "uv-fs", "uv-git-types", diff --git a/crates/uv-bench/benches/uv.rs b/crates/uv-bench/benches/uv.rs index 95106a52b..9bdd7adb9 100644 --- a/crates/uv-bench/benches/uv.rs +++ b/crates/uv-bench/benches/uv.rs @@ -91,7 +91,7 @@ mod resolver { }; use uv_dispatch::{BuildDispatch, SharedState}; use uv_distribution::DistributionDatabase; - use uv_distribution_types::{DependencyMetadata, IndexLocations}; + use uv_distribution_types::{DependencyMetadata, IndexLocations, RequiresPython}; use uv_install_wheel::LinkMode; use uv_pep440::Version; use uv_pep508::{MarkerEnvironment, MarkerEnvironmentBuilder}; @@ -99,8 +99,8 @@ mod resolver { use uv_pypi_types::{Conflicts, ResolverMarkerEnvironment}; use uv_python::Interpreter; use uv_resolver::{ - FlatIndex, InMemoryIndex, Manifest, OptionsBuilder, PythonRequirement, RequiresPython, - Resolver, ResolverEnvironment, ResolverOutput, + FlatIndex, InMemoryIndex, Manifest, OptionsBuilder, PythonRequirement, Resolver, + ResolverEnvironment, ResolverOutput, }; use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy}; use uv_workspace::WorkspaceCache; diff --git a/crates/uv-configuration/src/dependency_groups.rs b/crates/uv-configuration/src/dependency_groups.rs index 345f4077c..a3b90ea5f 100644 --- a/crates/uv-configuration/src/dependency_groups.rs +++ b/crates/uv-configuration/src/dependency_groups.rs @@ -295,6 +295,15 @@ pub struct DependencyGroupsWithDefaults { } impl DependencyGroupsWithDefaults { + /// Do not enable any groups + /// + /// Many places in the code need to know what dependency-groups are active, + /// but various commands or subsystems never enable any dependency-groups, + /// in which case they want this. + pub fn none() -> Self { + DependencyGroups::default().with_defaults(DefaultGroups::default()) + } + /// Returns `true` if the specification was enabled, and *only* because it was a default pub fn contains_because_default(&self, group: &GroupName) -> bool { self.cur.contains(group) && !self.prev.contains(group) diff --git a/crates/uv-configuration/src/extras.rs b/crates/uv-configuration/src/extras.rs index 3bc9da21a..e39fc72ef 100644 --- a/crates/uv-configuration/src/extras.rs +++ b/crates/uv-configuration/src/extras.rs @@ -263,6 +263,14 @@ pub struct ExtrasSpecificationWithDefaults { } impl ExtrasSpecificationWithDefaults { + /// Do not enable any extras + /// + /// Many places in the code need to know what extras are active, + /// but various commands or subsystems never enable any extras, + /// in which case they want this. + pub fn none() -> Self { + ExtrasSpecification::default().with_defaults(DefaultExtras::default()) + } /// Returns `true` if the specification was enabled, and *only* because it was a default pub fn contains_because_default(&self, extra: &ExtraName) -> bool { self.cur.contains(extra) && !self.prev.contains(extra) diff --git a/crates/uv-distribution-types/src/lib.rs b/crates/uv-distribution-types/src/lib.rs index 44030ffee..1e3ad7eba 100644 --- a/crates/uv-distribution-types/src/lib.rs +++ b/crates/uv-distribution-types/src/lib.rs @@ -73,6 +73,7 @@ pub use crate::pip_index::*; pub use crate::prioritized_distribution::*; pub use crate::requested::*; pub use crate::requirement::*; +pub use crate::requires_python::*; pub use crate::resolution::*; pub use crate::resolved::*; pub use crate::specified_requirement::*; @@ -100,6 +101,7 @@ mod pip_index; mod prioritized_distribution; mod requested; mod requirement; +mod requires_python; mod resolution; mod resolved; mod specified_requirement; diff --git a/crates/uv-resolver/src/requires_python.rs b/crates/uv-distribution-types/src/requires_python.rs similarity index 97% rename from crates/uv-resolver/src/requires_python.rs rename to crates/uv-distribution-types/src/requires_python.rs index 8e4d33213..ae9fee7fe 100644 --- a/crates/uv-resolver/src/requires_python.rs +++ b/crates/uv-distribution-types/src/requires_python.rs @@ -1,6 +1,6 @@ use std::collections::Bound; -use pubgrub::Range; +use version_ranges::Ranges; use uv_distribution_filename::WheelFilename; use uv_pep440::{ @@ -68,7 +68,7 @@ impl RequiresPython { let range = specifiers .into_iter() .map(|specifier| release_specifiers_to_ranges(specifier.clone())) - .fold(None, |range: Option>, requires_python| { + .fold(None, |range: Option>, requires_python| { if let Some(range) = range { Some(range.intersection(&requires_python)) } else { @@ -97,12 +97,12 @@ impl RequiresPython { pub fn split(&self, bound: Bound) -> Option<(Self, Self)> { let RequiresPythonRange(.., upper) = &self.range; - let upper = Range::from_range_bounds((bound, upper.clone().into())); + let upper = Ranges::from_range_bounds((bound, upper.clone().into())); let lower = upper.complement(); // Intersect left and right with the existing range. - let lower = lower.intersection(&Range::from(self.range.clone())); - let upper = upper.intersection(&Range::from(self.range.clone())); + let lower = lower.intersection(&Ranges::from(self.range.clone())); + let upper = upper.intersection(&Ranges::from(self.range.clone())); if lower.is_empty() || upper.is_empty() { None @@ -353,7 +353,7 @@ impl RequiresPython { /// a lock file are deserialized and turned into a `ResolutionGraph`, the /// markers are "complexified" to put the `requires-python` assumption back /// into the marker explicitly. - pub(crate) fn simplify_markers(&self, marker: MarkerTree) -> MarkerTree { + pub fn simplify_markers(&self, marker: MarkerTree) -> MarkerTree { let (lower, upper) = (self.range().lower(), self.range().upper()); marker.simplify_python_versions(lower.as_ref(), upper.as_ref()) } @@ -373,7 +373,7 @@ impl RequiresPython { /// ```text /// python_full_version >= '3.8' and python_full_version < '3.12' /// ``` - pub(crate) fn complexify_markers(&self, marker: MarkerTree) -> MarkerTree { + pub fn complexify_markers(&self, marker: MarkerTree) -> MarkerTree { let (lower, upper) = (self.range().lower(), self.range().upper()); marker.complexify_python_versions(lower.as_ref(), upper.as_ref()) } @@ -537,7 +537,7 @@ pub struct RequiresPythonRange(LowerBound, UpperBound); impl RequiresPythonRange { /// Initialize a [`RequiresPythonRange`] from a [`Range`]. - pub fn from_range(range: &Range) -> Self { + pub fn from_range(range: &Ranges) -> Self { let (lower, upper) = range .bounding_range() .map(|(lower_bound, upper_bound)| (lower_bound.cloned(), upper_bound.cloned())) @@ -575,9 +575,9 @@ impl Default for RequiresPythonRange { } } -impl From for Range { +impl From for Ranges { fn from(value: RequiresPythonRange) -> Self { - Range::from_range_bounds::<(Bound, Bound), _>(( + Ranges::from_range_bounds::<(Bound, Bound), _>(( value.0.into(), value.1.into(), )) @@ -592,21 +592,18 @@ impl From for Range { /// a simplified marker, one must re-contextualize it by adding the /// `requires-python` constraint back to the marker. #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, PartialOrd, Ord, serde::Deserialize)] -pub(crate) struct SimplifiedMarkerTree(MarkerTree); +pub struct SimplifiedMarkerTree(MarkerTree); impl SimplifiedMarkerTree { /// Simplifies the given markers by assuming the given `requires-python` /// bound is true. - pub(crate) fn new( - requires_python: &RequiresPython, - marker: MarkerTree, - ) -> SimplifiedMarkerTree { + pub fn new(requires_python: &RequiresPython, marker: MarkerTree) -> SimplifiedMarkerTree { SimplifiedMarkerTree(requires_python.simplify_markers(marker)) } /// Complexifies the given markers by adding the given `requires-python` as /// a constraint to these simplified markers. - pub(crate) fn into_marker(self, requires_python: &RequiresPython) -> MarkerTree { + pub fn into_marker(self, requires_python: &RequiresPython) -> MarkerTree { requires_python.complexify_markers(self.0) } @@ -614,12 +611,12 @@ impl SimplifiedMarkerTree { /// /// This only returns `None` when the underlying marker is always true, /// i.e., it matches all possible marker environments. - pub(crate) fn try_to_string(self) -> Option { + pub fn try_to_string(self) -> Option { self.0.try_to_string() } /// Returns the underlying marker tree without re-complexifying them. - pub(crate) fn as_simplified_marker_tree(self) -> MarkerTree { + pub fn as_simplified_marker_tree(self) -> MarkerTree { self.0 } } diff --git a/crates/uv-distribution/src/metadata/requires_dist.rs b/crates/uv-distribution/src/metadata/requires_dist.rs index d728ed58b..e9f36f174 100644 --- a/crates/uv-distribution/src/metadata/requires_dist.rs +++ b/crates/uv-distribution/src/metadata/requires_dist.rs @@ -6,7 +6,7 @@ use rustc_hash::FxHashSet; use uv_configuration::SourceStrategy; use uv_distribution_types::{IndexLocations, Requirement}; -use uv_normalize::{DEV_DEPENDENCIES, ExtraName, GroupName, PackageName}; +use uv_normalize::{ExtraName, GroupName, PackageName}; use uv_pep508::MarkerTree; use uv_workspace::dependency_groups::FlatDependencyGroups; use uv_workspace::pyproject::{Sources, ToolUvSources}; @@ -107,41 +107,10 @@ impl RequiresDist { SourceStrategy::Disabled => &empty, }; - // Collect the dependency groups. - let dependency_groups = { - // First, collect `tool.uv.dev_dependencies` - let dev_dependencies = project_workspace - .current_project() - .pyproject_toml() - .tool - .as_ref() - .and_then(|tool| tool.uv.as_ref()) - .and_then(|uv| uv.dev_dependencies.as_ref()); - - // Then, collect `dependency-groups` - let dependency_groups = project_workspace - .current_project() - .pyproject_toml() - .dependency_groups - .iter() - .flatten() - .collect::>(); - - // Flatten the dependency groups. - let mut dependency_groups = - FlatDependencyGroups::from_dependency_groups(&dependency_groups) - .map_err(|err| err.with_dev_dependencies(dev_dependencies))?; - - // Add the `dev` group, if `dev-dependencies` is defined. - if let Some(dev_dependencies) = dev_dependencies { - dependency_groups - .entry(DEV_DEPENDENCIES.clone()) - .or_insert_with(Vec::new) - .extend(dev_dependencies.clone()); - } - - dependency_groups - }; + let dependency_groups = FlatDependencyGroups::from_pyproject_toml( + project_workspace.current_project().root(), + project_workspace.current_project().pyproject_toml(), + )?; // Now that we've resolved the dependency groups, we can validate that each source references // a valid extra or group, if present. @@ -150,9 +119,10 @@ impl RequiresDist { // Lower the dependency groups. let dependency_groups = dependency_groups .into_iter() - .map(|(name, requirements)| { + .map(|(name, flat_group)| { let requirements = match source_strategy { - SourceStrategy::Enabled => requirements + SourceStrategy::Enabled => flat_group + .requirements .into_iter() .flat_map(|requirement| { let requirement_name = requirement.name.clone(); @@ -182,9 +152,11 @@ impl RequiresDist { ) }) .collect::, _>>(), - SourceStrategy::Disabled => { - Ok(requirements.into_iter().map(Requirement::from).collect()) - } + SourceStrategy::Disabled => Ok(flat_group + .requirements + .into_iter() + .map(Requirement::from) + .collect()), }?; Ok::<(GroupName, Box<_>), MetadataError>((name, requirements)) }) @@ -265,7 +237,7 @@ impl RequiresDist { if let Some(group) = source.group() { // If the group doesn't exist at all, error. - let Some(dependencies) = dependency_groups.get(group) else { + let Some(flat_group) = dependency_groups.get(group) else { return Err(MetadataError::MissingSourceGroup( name.clone(), group.clone(), @@ -273,7 +245,8 @@ impl RequiresDist { }; // If there is no such requirement with the group, error. - if !dependencies + if !flat_group + .requirements .iter() .any(|requirement| requirement.name == *name) { diff --git a/crates/uv-resolver/src/lib.rs b/crates/uv-resolver/src/lib.rs index 3285f9a6a..48904660d 100644 --- a/crates/uv-resolver/src/lib.rs +++ b/crates/uv-resolver/src/lib.rs @@ -14,7 +14,6 @@ pub use options::{Flexibility, Options, OptionsBuilder}; pub use preferences::{Preference, PreferenceError, Preferences}; pub use prerelease::PrereleaseMode; pub use python_requirement::PythonRequirement; -pub use requires_python::{RequiresPython, RequiresPythonRange}; pub use resolution::{ AnnotationStyle, ConflictingDistributionError, DisplayResolutionGraph, ResolverOutput, }; @@ -58,7 +57,6 @@ mod prerelease; mod pubgrub; mod python_requirement; mod redirect; -mod requires_python; mod resolution; mod resolution_mode; mod resolver; diff --git a/crates/uv-resolver/src/lock/export/pylock_toml.rs b/crates/uv-resolver/src/lock/export/pylock_toml.rs index 4f3e885ab..d2c2383a5 100644 --- a/crates/uv-resolver/src/lock/export/pylock_toml.rs +++ b/crates/uv-resolver/src/lock/export/pylock_toml.rs @@ -23,8 +23,8 @@ use uv_distribution_filename::{ use uv_distribution_types::{ BuiltDist, DirectUrlBuiltDist, DirectUrlSourceDist, DirectorySourceDist, Dist, Edge, FileLocation, GitSourceDist, IndexUrl, Name, Node, PathBuiltDist, PathSourceDist, - RegistryBuiltDist, RegistryBuiltWheel, RegistrySourceDist, RemoteSource, Resolution, - ResolvedDist, SourceDist, ToUrlError, UrlString, + RegistryBuiltDist, RegistryBuiltWheel, RegistrySourceDist, RemoteSource, RequiresPython, + Resolution, ResolvedDist, SourceDist, ToUrlError, UrlString, }; use uv_fs::{PortablePathBuf, relative_to}; use uv_git::{RepositoryReference, ResolvedRepositoryReference}; @@ -40,7 +40,7 @@ use uv_small_str::SmallString; use crate::lock::export::ExportableRequirements; use crate::lock::{Source, WheelTagHint, each_element_on_its_line_array}; use crate::resolution::ResolutionGraphNode; -use crate::{Installable, LockError, RequiresPython, ResolverOutput}; +use crate::{Installable, LockError, ResolverOutput}; #[derive(Debug, thiserror::Error)] pub enum PylockTomlErrorKind { diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index faacae736..47597f2ec 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -29,8 +29,8 @@ use uv_distribution_types::{ BuiltDist, DependencyMetadata, DirectUrlBuiltDist, DirectUrlSourceDist, DirectorySourceDist, Dist, DistributionMetadata, FileLocation, GitSourceDist, IndexLocations, IndexMetadata, IndexUrl, Name, PathBuiltDist, PathSourceDist, RegistryBuiltDist, RegistryBuiltWheel, - RegistrySourceDist, RemoteSource, Requirement, RequirementSource, ResolvedDist, StaticMetadata, - ToUrlError, UrlString, + RegistrySourceDist, RemoteSource, Requirement, RequirementSource, RequiresPython, ResolvedDist, + SimplifiedMarkerTree, StaticMetadata, ToUrlError, UrlString, }; use uv_fs::{PortablePath, PortablePathBuf, relative_to}; use uv_git::{RepositoryReference, ResolvedRepositoryReference}; @@ -57,12 +57,10 @@ pub use crate::lock::export::{PylockToml, PylockTomlErrorKind}; pub use crate::lock::installable::Installable; pub use crate::lock::map::PackageMap; pub use crate::lock::tree::TreeDisplay; -use crate::requires_python::SimplifiedMarkerTree; use crate::resolution::{AnnotatedDist, ResolutionGraphNode}; use crate::universal_marker::{ConflictMarker, UniversalMarker}; use crate::{ - ExcludeNewer, InMemoryIndex, MetadataResponse, PrereleaseMode, RequiresPython, ResolutionMode, - ResolverOutput, + ExcludeNewer, InMemoryIndex, MetadataResponse, PrereleaseMode, ResolutionMode, ResolverOutput, }; mod export; diff --git a/crates/uv-resolver/src/marker.rs b/crates/uv-resolver/src/marker.rs index 1bb938a33..b63d51401 100644 --- a/crates/uv-resolver/src/marker.rs +++ b/crates/uv-resolver/src/marker.rs @@ -5,7 +5,7 @@ use std::ops::Bound; use uv_pep440::{LowerBound, UpperBound, Version}; use uv_pep508::{CanonicalMarkerValueVersion, MarkerTree, MarkerTreeKind}; -use crate::requires_python::RequiresPythonRange; +use uv_distribution_types::RequiresPythonRange; /// Returns the bounding Python versions that can satisfy the [`MarkerTree`], if it's constrained. pub(crate) fn requires_python(tree: MarkerTree) -> Option { diff --git a/crates/uv-resolver/src/pubgrub/report.rs b/crates/uv-resolver/src/pubgrub/report.rs index b7b83a19b..91f8d4baa 100644 --- a/crates/uv-resolver/src/pubgrub/report.rs +++ b/crates/uv-resolver/src/pubgrub/report.rs @@ -11,7 +11,7 @@ use rustc_hash::FxHashMap; use uv_configuration::{IndexStrategy, NoBinary, NoBuild}; use uv_distribution_types::{ IncompatibleDist, IncompatibleSource, IncompatibleWheel, Index, IndexCapabilities, - IndexLocations, IndexMetadata, IndexUrl, + IndexLocations, IndexMetadata, IndexUrl, RequiresPython, }; use uv_normalize::PackageName; use uv_pep440::{Version, VersionSpecifiers}; @@ -27,9 +27,7 @@ use crate::python_requirement::{PythonRequirement, PythonRequirementSource}; use crate::resolver::{ MetadataUnavailable, UnavailablePackage, UnavailableReason, UnavailableVersion, }; -use crate::{ - Flexibility, InMemoryIndex, Options, RequiresPython, ResolverEnvironment, VersionsResponse, -}; +use crate::{Flexibility, InMemoryIndex, Options, ResolverEnvironment, VersionsResponse}; #[derive(Debug)] pub(crate) struct PubGrubReportFormatter<'a> { diff --git a/crates/uv-resolver/src/python_requirement.rs b/crates/uv-resolver/src/python_requirement.rs index 178b77866..0dce9b4f7 100644 --- a/crates/uv-resolver/src/python_requirement.rs +++ b/crates/uv-resolver/src/python_requirement.rs @@ -1,11 +1,10 @@ use std::collections::Bound; +use uv_distribution_types::{RequiresPython, RequiresPythonRange}; use uv_pep440::Version; use uv_pep508::{MarkerEnvironment, MarkerTree}; use uv_python::{Interpreter, PythonVersion}; -use crate::{RequiresPython, RequiresPythonRange}; - #[derive(Debug, Clone, Eq, PartialEq)] pub struct PythonRequirement { source: PythonRequirementSource, diff --git a/crates/uv-resolver/src/resolution/output.rs b/crates/uv-resolver/src/resolution/output.rs index 5df5ae6c3..928b9c605 100644 --- a/crates/uv-resolver/src/resolution/output.rs +++ b/crates/uv-resolver/src/resolution/output.rs @@ -12,8 +12,8 @@ use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet}; use uv_configuration::{Constraints, Overrides}; use uv_distribution::Metadata; use uv_distribution_types::{ - Dist, DistributionMetadata, Edge, IndexUrl, Name, Node, Requirement, ResolutionDiagnostic, - ResolvedDist, VersionId, VersionOrUrlRef, + Dist, DistributionMetadata, Edge, IndexUrl, Name, Node, Requirement, RequiresPython, + ResolutionDiagnostic, ResolvedDist, VersionId, VersionOrUrlRef, }; use uv_git::GitResolver; use uv_normalize::{ExtraName, GroupName, PackageName}; @@ -30,8 +30,7 @@ use crate::resolution_mode::ResolutionStrategy; use crate::resolver::{Resolution, ResolutionDependencyEdge, ResolutionPackage}; use crate::universal_marker::{ConflictMarker, UniversalMarker}; use crate::{ - InMemoryIndex, MetadataResponse, Options, PythonRequirement, RequiresPython, ResolveError, - VersionsResponse, + InMemoryIndex, MetadataResponse, Options, PythonRequirement, ResolveError, VersionsResponse, }; /// The output of a successful resolution. diff --git a/crates/uv-resolver/src/resolution/requirements_txt.rs b/crates/uv-resolver/src/resolution/requirements_txt.rs index 5ad6480c2..bcdef207b 100644 --- a/crates/uv-resolver/src/resolution/requirements_txt.rs +++ b/crates/uv-resolver/src/resolution/requirements_txt.rs @@ -4,16 +4,16 @@ use std::path::Path; use itertools::Itertools; -use uv_distribution_types::{DistributionMetadata, Name, ResolvedDist, Verbatim, VersionOrUrlRef}; +use uv_distribution_types::{ + DistributionMetadata, Name, RequiresPython, ResolvedDist, SimplifiedMarkerTree, Verbatim, + VersionOrUrlRef, +}; use uv_normalize::{ExtraName, PackageName}; use uv_pep440::Version; use uv_pep508::{MarkerTree, Scheme, split_scheme}; use uv_pypi_types::HashDigest; -use crate::{ - requires_python::{RequiresPython, SimplifiedMarkerTree}, - resolution::AnnotatedDist, -}; +use crate::resolution::AnnotatedDist; #[derive(Debug, Clone)] /// A pinned package with its resolved distribution and all the extras that were pinned for it. diff --git a/crates/uv-resolver/src/resolver/environment.rs b/crates/uv-resolver/src/resolver/environment.rs index 354941886..6e816f991 100644 --- a/crates/uv-resolver/src/resolver/environment.rs +++ b/crates/uv-resolver/src/resolver/environment.rs @@ -1,14 +1,14 @@ use std::sync::Arc; use tracing::trace; +use uv_distribution_types::{RequiresPython, RequiresPythonRange}; use uv_pep440::VersionSpecifiers; use uv_pep508::{MarkerEnvironment, MarkerTree}; use uv_pypi_types::{ConflictItem, ConflictItemRef, ResolverMarkerEnvironment}; use crate::pubgrub::{PubGrubDependency, PubGrubPackage}; -use crate::requires_python::RequiresPythonRange; use crate::resolver::ForkState; use crate::universal_marker::{ConflictMarker, UniversalMarker}; -use crate::{PythonRequirement, RequiresPython, ResolveError}; +use crate::{PythonRequirement, ResolveError}; /// Represents one or more marker environments for a resolution. /// @@ -628,7 +628,7 @@ mod tests { use uv_pep440::{LowerBound, UpperBound, Version}; use uv_pep508::{MarkerEnvironment, MarkerEnvironmentBuilder}; - use crate::requires_python::{RequiresPython, RequiresPythonRange}; + use uv_distribution_types::{RequiresPython, RequiresPythonRange}; use super::*; diff --git a/crates/uv-resolver/src/resolver/provider.rs b/crates/uv-resolver/src/resolver/provider.rs index 378d2a9eb..d6384e3e2 100644 --- a/crates/uv-resolver/src/resolver/provider.rs +++ b/crates/uv-resolver/src/resolver/provider.rs @@ -5,16 +5,17 @@ use uv_configuration::BuildOptions; use uv_distribution::{ArchiveMetadata, DistributionDatabase, Reporter}; use uv_distribution_types::{ Dist, IndexCapabilities, IndexMetadata, IndexMetadataRef, InstalledDist, RequestedDist, + RequiresPython, }; use uv_normalize::PackageName; use uv_pep440::{Version, VersionSpecifiers}; use uv_platform_tags::Tags; use uv_types::{BuildContext, HashStrategy}; +use crate::ExcludeNewer; use crate::flat_index::FlatIndex; use crate::version_map::VersionMap; use crate::yanks::AllowedYanks; -use crate::{ExcludeNewer, RequiresPython}; pub type PackageVersionsResult = Result; pub type WheelMetadataResult = Result; diff --git a/crates/uv-resolver/src/version_map.rs b/crates/uv-resolver/src/version_map.rs index 44e70e73b..8a0b17fc4 100644 --- a/crates/uv-resolver/src/version_map.rs +++ b/crates/uv-resolver/src/version_map.rs @@ -11,7 +11,8 @@ use uv_configuration::BuildOptions; use uv_distribution_filename::{DistFilename, WheelFilename}; use uv_distribution_types::{ HashComparison, IncompatibleSource, IncompatibleWheel, IndexUrl, PrioritizedDist, - RegistryBuiltWheel, RegistrySourceDist, SourceDistCompatibility, WheelCompatibility, + RegistryBuiltWheel, RegistrySourceDist, RequiresPython, SourceDistCompatibility, + WheelCompatibility, }; use uv_normalize::PackageName; use uv_pep440::Version; @@ -21,7 +22,7 @@ use uv_types::HashStrategy; use uv_warnings::warn_user_once; use crate::flat_index::FlatDistributions; -use crate::{ExcludeNewer, RequiresPython, yanks::AllowedYanks}; +use crate::{ExcludeNewer, yanks::AllowedYanks}; /// A map from versions to distributions. #[derive(Debug)] diff --git a/crates/uv-settings/src/settings.rs b/crates/uv-settings/src/settings.rs index ff6d39995..2c18fb40a 100644 --- a/crates/uv-settings/src/settings.rs +++ b/crates/uv-settings/src/settings.rs @@ -140,6 +140,9 @@ pub struct Options { #[cfg_attr(feature = "schemars", schemars(skip))] pub default_groups: Option, + #[cfg_attr(feature = "schemars", schemars(skip))] + pub dependency_groups: Option, + #[cfg_attr(feature = "schemars", schemars(skip))] pub managed: Option, @@ -1870,6 +1873,7 @@ pub struct OptionsWire { managed: Option, r#package: Option, default_groups: Option, + dependency_groups: Option, dev_dependencies: Option, // Build backend @@ -1934,6 +1938,7 @@ impl From for Options { workspace, sources, default_groups, + dependency_groups, dev_dependencies, managed, package, @@ -2010,6 +2015,7 @@ impl From for Options { sources, dev_dependencies, default_groups, + dependency_groups, managed, package, } diff --git a/crates/uv-workspace/Cargo.toml b/crates/uv-workspace/Cargo.toml index a8d672aab..36059f10f 100644 --- a/crates/uv-workspace/Cargo.toml +++ b/crates/uv-workspace/Cargo.toml @@ -18,6 +18,7 @@ workspace = true [dependencies] uv-build-backend = { workspace = true, features = ["schemars"] } uv-cache-key = { workspace = true } +uv-configuration = { workspace = true } uv-distribution-types = { workspace = true } uv-fs = { workspace = true, features = ["tokio", "schemars"] } uv-git-types = { workspace = true } diff --git a/crates/uv-workspace/src/dependency_groups.rs b/crates/uv-workspace/src/dependency_groups.rs index e6964544a..8503ae3ad 100644 --- a/crates/uv-workspace/src/dependency_groups.rs +++ b/crates/uv-workspace/src/dependency_groups.rs @@ -1,32 +1,106 @@ -use std::collections::BTreeMap; use std::collections::btree_map::Entry; use std::str::FromStr; +use std::{collections::BTreeMap, path::Path}; use thiserror::Error; use tracing::error; +use uv_distribution_types::RequiresPython; +use uv_fs::Simplified; use uv_normalize::{DEV_DEPENDENCIES, GroupName}; +use uv_pep440::VersionSpecifiers; use uv_pep508::Pep508Error; use uv_pypi_types::{DependencyGroupSpecifier, VerbatimParsedUrl}; +use crate::pyproject::{DependencyGroupSettings, PyProjectToml, ToolUvDependencyGroups}; + /// PEP 735 dependency groups, with any `include-group` entries resolved. #[derive(Debug, Default, Clone)] -pub struct FlatDependencyGroups( - BTreeMap>>, -); +pub struct FlatDependencyGroups(BTreeMap); + +#[derive(Debug, Default, Clone)] +pub struct FlatDependencyGroup { + pub requirements: Vec>, + pub requires_python: Option, +} impl FlatDependencyGroups { + /// Gather and flatten all the dependency-groups defined in the given pyproject.toml + /// + /// The path is only used in diagnostics. + pub fn from_pyproject_toml( + path: &Path, + pyproject_toml: &PyProjectToml, + ) -> Result { + // First, collect `tool.uv.dev_dependencies` + let dev_dependencies = pyproject_toml + .tool + .as_ref() + .and_then(|tool| tool.uv.as_ref()) + .and_then(|uv| uv.dev_dependencies.as_ref()); + + // Then, collect `dependency-groups` + let dependency_groups = pyproject_toml + .dependency_groups + .iter() + .flatten() + .collect::>(); + + // Get additional settings + let empty_settings = ToolUvDependencyGroups::default(); + let group_settings = pyproject_toml + .tool + .as_ref() + .and_then(|tool| tool.uv.as_ref()) + .and_then(|uv| uv.dependency_groups.as_ref()) + .unwrap_or(&empty_settings); + + // Flatten the dependency groups. + let mut dependency_groups = FlatDependencyGroups::from_dependency_groups( + &dependency_groups, + group_settings.inner(), + ) + .map_err(|err| DependencyGroupError { + package: pyproject_toml + .project + .as_ref() + .map(|project| project.name.to_string()) + .unwrap_or_default(), + path: path.user_display().to_string(), + error: err.with_dev_dependencies(dev_dependencies), + })?; + + // Add the `dev` group, if the legacy `dev-dependencies` is defined. + // + // NOTE: the fact that we do this out here means that nothing can inherit from + // the legacy dev-dependencies group (or define a group requires-python for it). + // This is intentional, we want groups to be defined in a standard interoperable + // way, and letting things include-group a group that isn't defined would be a + // mess for other python tools. + if let Some(dev_dependencies) = dev_dependencies { + dependency_groups + .entry(DEV_DEPENDENCIES.clone()) + .or_insert_with(FlatDependencyGroup::default) + .requirements + .extend(dev_dependencies.clone()); + } + + Ok(dependency_groups) + } + /// Resolve the dependency groups (which may contain references to other groups) into concrete /// lists of requirements. - pub fn from_dependency_groups( + fn from_dependency_groups( groups: &BTreeMap<&GroupName, &Vec>, - ) -> Result { + settings: &BTreeMap, + ) -> Result { fn resolve_group<'data>( - resolved: &mut BTreeMap>>, + resolved: &mut BTreeMap, groups: &'data BTreeMap<&GroupName, &Vec>, + settings: &BTreeMap, name: &'data GroupName, parents: &mut Vec<&'data GroupName>, - ) -> Result<(), DependencyGroupError> { + ) -> Result<(), DependencyGroupErrorInner> { let Some(specifiers) = groups.get(name) else { // Missing group let parent_name = parents @@ -34,7 +108,7 @@ impl FlatDependencyGroups { .last() .copied() .expect("parent when group is missing"); - return Err(DependencyGroupError::GroupNotFound( + return Err(DependencyGroupErrorInner::GroupNotFound( name.clone(), parent_name.clone(), )); @@ -42,7 +116,7 @@ impl FlatDependencyGroups { // "Dependency Group Includes MUST NOT include cycles, and tools SHOULD report an error if they detect a cycle." if parents.contains(&name) { - return Err(DependencyGroupError::DependencyGroupCycle(Cycle( + return Err(DependencyGroupErrorInner::DependencyGroupCycle(Cycle( parents.iter().copied().cloned().collect(), ))); } @@ -54,13 +128,14 @@ impl FlatDependencyGroups { parents.push(name); let mut requirements = Vec::with_capacity(specifiers.len()); + let mut requires_python_intersection = VersionSpecifiers::empty(); for specifier in *specifiers { match specifier { DependencyGroupSpecifier::Requirement(requirement) => { match uv_pep508::Requirement::::from_str(requirement) { Ok(requirement) => requirements.push(requirement), Err(err) => { - return Err(DependencyGroupError::GroupParseError( + return Err(DependencyGroupErrorInner::GroupParseError( name.clone(), requirement.clone(), Box::new(err), @@ -69,72 +144,107 @@ impl FlatDependencyGroups { } } DependencyGroupSpecifier::IncludeGroup { include_group } => { - resolve_group(resolved, groups, include_group, parents)?; - requirements - .extend(resolved.get(include_group).into_iter().flatten().cloned()); + resolve_group(resolved, groups, settings, include_group, parents)?; + if let Some(included) = resolved.get(include_group) { + requirements.extend(included.requirements.iter().cloned()); + + // Intersect the requires-python for this group with the the included group's + requires_python_intersection = requires_python_intersection + .into_iter() + .chain(included.requires_python.clone().into_iter().flatten()) + .collect(); + } } DependencyGroupSpecifier::Object(map) => { - return Err(DependencyGroupError::DependencyObjectSpecifierNotSupported( - name.clone(), - map.clone(), - )); + return Err( + DependencyGroupErrorInner::DependencyObjectSpecifierNotSupported( + name.clone(), + map.clone(), + ), + ); } } } + + let empty_settings = DependencyGroupSettings::default(); + let DependencyGroupSettings { requires_python } = + settings.get(name).unwrap_or(&empty_settings); + if let Some(requires_python) = requires_python { + // Intersect the requires-python for this group to get the final requires-python + // that will be used by interpreter discovery and checking. + requires_python_intersection = requires_python_intersection + .into_iter() + .chain(requires_python.clone()) + .collect(); + + // Add the group requires-python as a marker to each requirement + // We don't use `requires_python_intersection` because each `include-group` + // should already have its markers applied to these. + for requirement in &mut requirements { + let extra_markers = + RequiresPython::from_specifiers(requires_python).to_marker_tree(); + requirement.marker.and(extra_markers); + } + } + parents.pop(); - resolved.insert(name.clone(), requirements); + resolved.insert( + name.clone(), + FlatDependencyGroup { + requirements, + requires_python: if requires_python_intersection.is_empty() { + None + } else { + Some(requires_python_intersection) + }, + }, + ); Ok(()) } + // Validate the settings + for (group_name, ..) in settings { + if !groups.contains_key(group_name) { + return Err(DependencyGroupErrorInner::SettingsGroupNotFound( + group_name.clone(), + )); + } + } + let mut resolved = BTreeMap::new(); for name in groups.keys() { let mut parents = Vec::new(); - resolve_group(&mut resolved, groups, name, &mut parents)?; + resolve_group(&mut resolved, groups, settings, name, &mut parents)?; } Ok(Self(resolved)) } /// Return the requirements for a given group, if any. - pub fn get( - &self, - group: &GroupName, - ) -> Option<&Vec>> { + pub fn get(&self, group: &GroupName) -> Option<&FlatDependencyGroup> { self.0.get(group) } /// Return the entry for a given group, if any. - pub fn entry( - &mut self, - group: GroupName, - ) -> Entry>> { + pub fn entry(&mut self, group: GroupName) -> Entry { self.0.entry(group) } /// Consume the [`FlatDependencyGroups`] and return the inner map. - pub fn into_inner(self) -> BTreeMap>> { + pub fn into_inner(self) -> BTreeMap { self.0 } } -impl FromIterator<(GroupName, Vec>)> - for FlatDependencyGroups -{ - fn from_iter< - T: IntoIterator>)>, - >( - iter: T, - ) -> Self { +impl FromIterator<(GroupName, FlatDependencyGroup)> for FlatDependencyGroups { + fn from_iter>(iter: T) -> Self { Self(iter.into_iter().collect()) } } impl IntoIterator for FlatDependencyGroups { - type Item = (GroupName, Vec>); - type IntoIter = std::collections::btree_map::IntoIter< - GroupName, - Vec>, - >; + type Item = (GroupName, FlatDependencyGroup); + type IntoIter = std::collections::btree_map::IntoIter; fn into_iter(self) -> Self::IntoIter { self.0.into_iter() @@ -142,7 +252,24 @@ impl IntoIterator for FlatDependencyGroups { } #[derive(Debug, Error)] -pub enum DependencyGroupError { +#[error("{} has malformed dependency groups", if path.is_empty() && package.is_empty() { + "Project".to_string() +} else if path.is_empty() { + format!("Project `{package}`") +} else if package.is_empty() { + format!("`{path}`") +} else { + format!("Project `{package} @ {path}`") +})] +pub struct DependencyGroupError { + package: String, + path: String, + #[source] + error: DependencyGroupErrorInner, +} + +#[derive(Debug, Error)] +pub enum DependencyGroupErrorInner { #[error("Failed to parse entry in group `{0}`: `{1}`")] GroupParseError( GroupName, @@ -159,9 +286,15 @@ pub enum DependencyGroupError { DependencyGroupCycle(Cycle), #[error("Group `{0}` contains an unknown dependency object specifier: {1:?}")] DependencyObjectSpecifierNotSupported(GroupName, BTreeMap), + #[error("Failed to find group `{0}` specified in `[tool.uv.dependency-groups]`")] + SettingsGroupNotFound(GroupName), + #[error( + "`[tool.uv.dependency-groups]` specifies the `dev` group, but only `tool.uv.dev-dependencies` was found. To reference the `dev` group, remove the `tool.uv.dev-dependencies` section and add any development dependencies to the `dev` entry in the `[dependency-groups]` table instead." + )] + SettingsDevGroupInclude, } -impl DependencyGroupError { +impl DependencyGroupErrorInner { /// Enrich a [`DependencyGroupError`] with the `tool.uv.dev-dependencies` metadata, if applicable. #[must_use] pub fn with_dev_dependencies( @@ -169,10 +302,15 @@ impl DependencyGroupError { dev_dependencies: Option<&Vec>>, ) -> Self { match self { - DependencyGroupError::GroupNotFound(group, parent) + Self::GroupNotFound(group, parent) if dev_dependencies.is_some() && group == *DEV_DEPENDENCIES => { - DependencyGroupError::DevGroupInclude(parent) + Self::DevGroupInclude(parent) + } + Self::SettingsGroupNotFound(group) + if dev_dependencies.is_some() && group == *DEV_DEPENDENCIES => + { + Self::SettingsDevGroupInclude } _ => self, } diff --git a/crates/uv-workspace/src/lib.rs b/crates/uv-workspace/src/lib.rs index 83be6bd88..0e1b3974c 100644 --- a/crates/uv-workspace/src/lib.rs +++ b/crates/uv-workspace/src/lib.rs @@ -1,6 +1,6 @@ pub use workspace::{ - DiscoveryOptions, MemberDiscovery, ProjectWorkspace, VirtualProject, Workspace, WorkspaceCache, - WorkspaceError, WorkspaceMember, + DiscoveryOptions, MemberDiscovery, ProjectWorkspace, RequiresPythonSources, VirtualProject, + Workspace, WorkspaceCache, WorkspaceError, WorkspaceMember, }; pub mod dependency_groups; diff --git a/crates/uv-workspace/src/pyproject.rs b/crates/uv-workspace/src/pyproject.rs index 2b0e44c16..6499aad5d 100644 --- a/crates/uv-workspace/src/pyproject.rs +++ b/crates/uv-workspace/src/pyproject.rs @@ -353,6 +353,24 @@ pub struct ToolUv { )] pub default_groups: Option, + /// Additional settings for `dependency-groups`. + /// + /// Currently this can only be used to add `requires-python` constraints + /// to dependency groups (typically to inform uv that your dev tooling + /// has a higher python requirement than your actual project). + /// + /// This cannot be used to define dependency groups, use the top-level + /// `[dependency-groups]` table for that. + #[option( + default = "[]", + value_type = "dict", + example = r#" + [tool.uv.dependency-groups] + my-group = {requires-python = ">=3.12"} + "# + )] + pub dependency_groups: Option, + /// The project's development dependencies. /// /// Development dependencies will be installed by default in `uv run` and `uv sync`, but will @@ -653,6 +671,77 @@ impl<'de> serde::de::Deserialize<'de> for ToolUvSources { } } +#[derive(Default, Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(Serialize))] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +pub struct ToolUvDependencyGroups(BTreeMap); + +impl ToolUvDependencyGroups { + /// Returns the underlying `BTreeMap` of group names to settings. + pub fn inner(&self) -> &BTreeMap { + &self.0 + } + + /// Convert the [`ToolUvDependencyGroups`] into its inner `BTreeMap`. + #[must_use] + pub fn into_inner(self) -> BTreeMap { + self.0 + } +} + +/// Ensure that all keys in the TOML table are unique. +impl<'de> serde::de::Deserialize<'de> for ToolUvDependencyGroups { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct SourcesVisitor; + + impl<'de> serde::de::Visitor<'de> for SourcesVisitor { + type Value = ToolUvDependencyGroups; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a map with unique keys") + } + + fn visit_map(self, mut access: M) -> Result + where + M: serde::de::MapAccess<'de>, + { + let mut groups = BTreeMap::new(); + while let Some((key, value)) = + access.next_entry::()? + { + match groups.entry(key) { + std::collections::btree_map::Entry::Occupied(entry) => { + return Err(serde::de::Error::custom(format!( + "duplicate settings for dependency group `{}`", + entry.key() + ))); + } + std::collections::btree_map::Entry::Vacant(entry) => { + entry.insert(value); + } + } + } + Ok(ToolUvDependencyGroups(groups)) + } + } + + deserializer.deserialize_map(SourcesVisitor) + } +} + +#[derive(Deserialize, Default, Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(Serialize))] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[serde(rename_all = "kebab-case")] +pub struct DependencyGroupSettings { + /// Version of python to require when installing this group + #[cfg_attr(feature = "schemars", schemars(with = "Option"))] + pub requires_python: Option, +} + #[derive(Deserialize, OptionsMetadata, Default, Debug, Clone, PartialEq, Eq)] #[cfg_attr(test, derive(Serialize))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] diff --git a/crates/uv-workspace/src/workspace.rs b/crates/uv-workspace/src/workspace.rs index 3caaa8f8c..ed99d3d30 100644 --- a/crates/uv-workspace/src/workspace.rs +++ b/crates/uv-workspace/src/workspace.rs @@ -8,6 +8,7 @@ use glob::{GlobError, PatternError, glob}; use rustc_hash::{FxHashMap, FxHashSet}; use tracing::{debug, trace, warn}; +use uv_configuration::DependencyGroupsWithDefaults; use uv_distribution_types::{Index, Requirement, RequirementSource}; use uv_fs::{CWD, Simplified}; use uv_normalize::{DEV_DEPENDENCIES, GroupName, PackageName}; @@ -17,7 +18,7 @@ use uv_pypi_types::{Conflicts, SupportedEnvironments, VerbatimParsedUrl}; use uv_static::EnvVars; use uv_warnings::warn_user_once; -use crate::dependency_groups::{DependencyGroupError, FlatDependencyGroups}; +use crate::dependency_groups::{DependencyGroupError, FlatDependencyGroup, FlatDependencyGroups}; use crate::pyproject::{ Project, PyProjectToml, PyprojectTomlError, Sources, ToolUvSources, ToolUvWorkspace, }; @@ -95,6 +96,8 @@ pub struct DiscoveryOptions { pub members: MemberDiscovery, } +pub type RequiresPythonSources = BTreeMap<(PackageName, Option), VersionSpecifiers>; + /// A workspace, consisting of a root directory and members. See [`ProjectWorkspace`]. #[derive(Debug, Clone)] #[cfg_attr(test, derive(serde::Serialize))] @@ -413,15 +416,44 @@ impl Workspace { } /// Returns an iterator over the `requires-python` values for each member of the workspace. - pub fn requires_python(&self) -> impl Iterator { - self.packages().iter().filter_map(|(name, member)| { - member + pub fn requires_python( + &self, + groups: &DependencyGroupsWithDefaults, + ) -> Result { + let mut requires = RequiresPythonSources::new(); + for (name, member) in self.packages() { + // Get the top-level requires-python for this package, which is always active + // + // Arguably we could check groups.prod() to disable this, since, the requires-python + // of the project is *technically* not relevant if you're doing `--only-group`, but, + // that would be a big surprising change so let's *not* do that until someone asks! + let top_requires = member .pyproject_toml() .project .as_ref() .and_then(|project| project.requires_python.as_ref()) - .map(|requires_python| (name, requires_python)) - }) + .map(|requires_python| ((name.to_owned(), None), requires_python.clone())); + requires.extend(top_requires); + + // Get the requires-python for each enabled group on this package + // We need to do full flattening here because include-group can transfer requires-python + let dependency_groups = + FlatDependencyGroups::from_pyproject_toml(member.root(), &member.pyproject_toml)?; + let group_requires = + dependency_groups + .into_iter() + .filter_map(move |(group_name, flat_group)| { + if groups.contains(&group_name) { + flat_group.requires_python.map(|requires_python| { + ((name.to_owned(), Some(group_name)), requires_python) + }) + } else { + None + } + }); + requires.extend(group_requires); + } + Ok(requires) } /// Returns any requirements that are exclusive to the workspace root, i.e., not included in @@ -439,12 +471,9 @@ impl Workspace { /// corresponding `pyproject.toml`. /// /// Otherwise, returns an empty list. - pub fn dependency_groups( + pub fn workspace_dependency_groups( &self, - ) -> Result< - BTreeMap>>, - DependencyGroupError, - > { + ) -> Result, DependencyGroupError> { if self .packages .values() @@ -455,35 +484,10 @@ impl Workspace { Ok(BTreeMap::default()) } else { // Otherwise, return the dependency groups in the non-project workspace root. - // First, collect `tool.uv.dev_dependencies` - let dev_dependencies = self - .pyproject_toml - .tool - .as_ref() - .and_then(|tool| tool.uv.as_ref()) - .and_then(|uv| uv.dev_dependencies.as_ref()); - - // Then, collect `dependency-groups` - let dependency_groups = self - .pyproject_toml - .dependency_groups - .iter() - .flatten() - .collect::>(); - - // Flatten the dependency groups. - let mut dependency_groups = - FlatDependencyGroups::from_dependency_groups(&dependency_groups) - .map_err(|err| err.with_dev_dependencies(dev_dependencies))?; - - // Add the `dev` group, if `dev-dependencies` is defined. - if let Some(dev_dependencies) = dev_dependencies { - dependency_groups - .entry(DEV_DEPENDENCIES.clone()) - .or_insert_with(Vec::new) - .extend(dev_dependencies.clone()); - } - + let dependency_groups = FlatDependencyGroups::from_pyproject_toml( + &self.install_path, + &self.pyproject_toml, + )?; Ok(dependency_groups.into_inner()) } } @@ -1818,6 +1822,7 @@ mod tests { "managed": null, "package": null, "default-groups": null, + "dependency-groups": null, "dev-dependencies": null, "override-dependencies": null, "constraint-dependencies": null, @@ -1913,6 +1918,7 @@ mod tests { "managed": null, "package": null, "default-groups": null, + "dependency-groups": null, "dev-dependencies": null, "override-dependencies": null, "constraint-dependencies": null, @@ -2123,6 +2129,7 @@ mod tests { "managed": null, "package": null, "default-groups": null, + "dependency-groups": null, "dev-dependencies": null, "override-dependencies": null, "constraint-dependencies": null, @@ -2230,6 +2237,7 @@ mod tests { "managed": null, "package": null, "default-groups": null, + "dependency-groups": null, "dev-dependencies": null, "override-dependencies": null, "constraint-dependencies": null, @@ -2350,6 +2358,7 @@ mod tests { "managed": null, "package": null, "default-groups": null, + "dependency-groups": null, "dev-dependencies": null, "override-dependencies": null, "constraint-dependencies": null, @@ -2444,6 +2453,7 @@ mod tests { "managed": null, "package": null, "default-groups": null, + "dependency-groups": null, "dev-dependencies": null, "override-dependencies": null, "constraint-dependencies": null, diff --git a/crates/uv/src/commands/build_frontend.rs b/crates/uv/src/commands/build_frontend.rs index c601541da..dd174ca06 100644 --- a/crates/uv/src/commands/build_frontend.rs +++ b/crates/uv/src/commands/build_frontend.rs @@ -16,13 +16,16 @@ use uv_cache::{Cache, CacheBucket}; use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ BuildKind, BuildOptions, BuildOutput, Concurrency, ConfigSettings, Constraints, - HashCheckingMode, IndexStrategy, KeyringProviderType, PreviewMode, SourceStrategy, + DependencyGroupsWithDefaults, HashCheckingMode, IndexStrategy, KeyringProviderType, + PreviewMode, SourceStrategy, }; use uv_dispatch::{BuildDispatch, SharedState}; use uv_distribution_filename::{ DistFilename, SourceDistExtension, SourceDistFilename, WheelFilename, }; -use uv_distribution_types::{DependencyMetadata, Index, IndexLocations, SourceDist}; +use uv_distribution_types::{ + DependencyMetadata, Index, IndexLocations, RequiresPython, SourceDist, +}; use uv_fs::{Simplified, relative_to}; use uv_install_wheel::LinkMode; use uv_normalize::PackageName; @@ -33,7 +36,7 @@ use uv_python::{ VersionRequest, }; use uv_requirements::RequirementsSource; -use uv_resolver::{ExcludeNewer, FlatIndex, RequiresPython}; +use uv_resolver::{ExcludeNewer, FlatIndex}; use uv_settings::PythonInstallMirrors; use uv_types::{AnyErrorBuild, BuildContext, BuildIsolation, BuildStack, HashStrategy}; use uv_workspace::{DiscoveryOptions, Workspace, WorkspaceCache, WorkspaceError}; @@ -471,7 +474,8 @@ async fn build_package( // (3) `Requires-Python` in `pyproject.toml` if interpreter_request.is_none() { if let Ok(workspace) = workspace { - interpreter_request = find_requires_python(workspace)? + let groups = DependencyGroupsWithDefaults::none(); + interpreter_request = find_requires_python(workspace, &groups)? .as_ref() .map(RequiresPython::specifiers) .map(|specifiers| { diff --git a/crates/uv/src/commands/pip/compile.rs b/crates/uv/src/commands/pip/compile.rs index 8da16ef46..20a60416f 100644 --- a/crates/uv/src/commands/pip/compile.rs +++ b/crates/uv/src/commands/pip/compile.rs @@ -21,7 +21,7 @@ use uv_configuration::{KeyringProviderType, TargetTriple}; use uv_dispatch::{BuildDispatch, SharedState}; use uv_distribution_types::{ DependencyMetadata, HashGeneration, Index, IndexLocations, NameRequirementSpecification, - Origin, Requirement, UnresolvedRequirementSpecification, Verbatim, + Origin, Requirement, RequiresPython, UnresolvedRequirementSpecification, Verbatim, }; use uv_fs::{CWD, Simplified}; use uv_git::ResolvedRepositoryReference; @@ -38,8 +38,8 @@ use uv_requirements::{ }; use uv_resolver::{ AnnotationStyle, DependencyMode, DisplayResolutionGraph, ExcludeNewer, FlatIndex, ForkStrategy, - InMemoryIndex, OptionsBuilder, PrereleaseMode, PylockToml, PythonRequirement, RequiresPython, - ResolutionMode, ResolverEnvironment, + InMemoryIndex, OptionsBuilder, PrereleaseMode, PylockToml, PythonRequirement, ResolutionMode, + ResolverEnvironment, }; use uv_torch::{TorchMode, TorchStrategy}; use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy}; diff --git a/crates/uv/src/commands/pip/latest.rs b/crates/uv/src/commands/pip/latest.rs index ac3ce7d1f..25da8466c 100644 --- a/crates/uv/src/commands/pip/latest.rs +++ b/crates/uv/src/commands/pip/latest.rs @@ -3,10 +3,10 @@ use tracing::debug; use uv_client::{MetadataFormat, RegistryClient, VersionFiles}; use uv_distribution_filename::DistFilename; -use uv_distribution_types::{IndexCapabilities, IndexMetadataRef, IndexUrl}; +use uv_distribution_types::{IndexCapabilities, IndexMetadataRef, IndexUrl, RequiresPython}; use uv_normalize::PackageName; use uv_platform_tags::Tags; -use uv_resolver::{ExcludeNewer, PrereleaseMode, RequiresPython}; +use uv_resolver::{ExcludeNewer, PrereleaseMode}; use uv_warnings::warn_user_once; /// A client to fetch the latest version of a package from an index. diff --git a/crates/uv/src/commands/pip/list.rs b/crates/uv/src/commands/pip/list.rs index 48786d86c..824c9db2b 100644 --- a/crates/uv/src/commands/pip/list.rs +++ b/crates/uv/src/commands/pip/list.rs @@ -17,14 +17,16 @@ use uv_cli::ListFormat; use uv_client::{BaseClientBuilder, RegistryClientBuilder}; use uv_configuration::{Concurrency, IndexStrategy, KeyringProviderType}; use uv_distribution_filename::DistFilename; -use uv_distribution_types::{Diagnostic, IndexCapabilities, IndexLocations, InstalledDist, Name}; +use uv_distribution_types::{ + Diagnostic, IndexCapabilities, IndexLocations, InstalledDist, Name, RequiresPython, +}; use uv_fs::Simplified; use uv_installer::SitePackages; use uv_normalize::PackageName; use uv_pep440::Version; use uv_python::PythonRequest; use uv_python::{EnvironmentPreference, PythonEnvironment}; -use uv_resolver::{ExcludeNewer, PrereleaseMode, RequiresPython}; +use uv_resolver::{ExcludeNewer, PrereleaseMode}; use crate::commands::ExitStatus; use crate::commands::pip::latest::LatestClient; diff --git a/crates/uv/src/commands/pip/tree.rs b/crates/uv/src/commands/pip/tree.rs index 05290ffd0..aed364068 100644 --- a/crates/uv/src/commands/pip/tree.rs +++ b/crates/uv/src/commands/pip/tree.rs @@ -14,14 +14,14 @@ use uv_cache::{Cache, Refresh}; use uv_cache_info::Timestamp; use uv_client::{BaseClientBuilder, RegistryClientBuilder}; use uv_configuration::{Concurrency, IndexStrategy, KeyringProviderType}; -use uv_distribution_types::{Diagnostic, IndexCapabilities, IndexLocations, Name}; +use uv_distribution_types::{Diagnostic, IndexCapabilities, IndexLocations, Name, RequiresPython}; use uv_installer::SitePackages; use uv_normalize::PackageName; use uv_pep440::Version; use uv_pep508::{Requirement, VersionOrUrl}; use uv_pypi_types::{ResolutionMetadata, ResolverMarkerEnvironment, VerbatimParsedUrl}; use uv_python::{EnvironmentPreference, PythonEnvironment, PythonRequest}; -use uv_resolver::{ExcludeNewer, PrereleaseMode, RequiresPython}; +use uv_resolver::{ExcludeNewer, PrereleaseMode}; use crate::commands::ExitStatus; use crate::commands::pip::latest::LatestClient; diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index a4091504d..8e3c4a03a 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -17,8 +17,9 @@ use uv_cache::Cache; use uv_cache_key::RepositoryUrl; use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ - Concurrency, Constraints, DependencyGroups, DevMode, DryRun, EditableMode, ExtrasSpecification, - InstallOptions, PreviewMode, SourceStrategy, + Concurrency, Constraints, DependencyGroups, DependencyGroupsWithDefaults, DevMode, DryRun, + EditableMode, ExtrasSpecification, ExtrasSpecificationWithDefaults, InstallOptions, + PreviewMode, SourceStrategy, }; use uv_dispatch::BuildDispatch; use uv_distribution::DistributionDatabase; @@ -29,7 +30,7 @@ use uv_distribution_types::{ use uv_fs::{LockedFile, Simplified}; use uv_git::GIT_STORE; use uv_git_types::GitReference; -use uv_normalize::{DEV_DEPENDENCIES, DefaultExtras, PackageName}; +use uv_normalize::{DEV_DEPENDENCIES, DefaultExtras, DefaultGroups, PackageName}; use uv_pep508::{ExtraName, MarkerTree, UnnamedRequirement, VersionOrUrl}; use uv_pypi_types::{ParsedUrl, VerbatimParsedUrl}; use uv_python::{Interpreter, PythonDownloads, PythonEnvironment, PythonPreference, PythonRequest}; @@ -79,7 +80,7 @@ pub(crate) async fn add( rev: Option, tag: Option, branch: Option, - extras: Vec, + extras_of_dependency: Vec, package: Option, python: Option, install_mirrors: PythonInstallMirrors, @@ -122,6 +123,34 @@ pub(crate) async fn add( let reporter = PythonDownloadReporter::single(printer); + // Determine what defaults/extras we're explicitly enabling + let (extras, groups) = match &dependency_type { + DependencyType::Production => { + let extras = ExtrasSpecification::from_extra(vec![]); + let groups = DependencyGroups::from_dev_mode(DevMode::Exclude); + (extras, groups) + } + DependencyType::Dev => { + let extras = ExtrasSpecification::from_extra(vec![]); + let groups = DependencyGroups::from_dev_mode(DevMode::Include); + (extras, groups) + } + DependencyType::Optional(extra_name) => { + let extras = ExtrasSpecification::from_extra(vec![extra_name.clone()]); + let groups = DependencyGroups::from_dev_mode(DevMode::Exclude); + (extras, groups) + } + DependencyType::Group(group_name) => { + let extras = ExtrasSpecification::from_extra(vec![]); + let groups = DependencyGroups::from_group(group_name.clone()); + (extras, groups) + } + }; + // Default extras currently always disabled + let defaulted_extras = extras.with_defaults(DefaultExtras::default()); + // Default groups we need the actual project for, interpreter discovery will use this! + let defaulted_groups; + let target = if let Some(script) = script { // If we found a PEP 723 script and the user provided a project-only setting, warn. if package.is_some() { @@ -172,6 +201,9 @@ pub(crate) async fn add( } }; + // Scripts don't actually have groups + defaulted_groups = groups.with_defaults(DefaultGroups::default()); + // Discover the interpreter. let interpreter = ScriptInterpreter::discover( Pep723ItemRef::Script(&script), @@ -234,11 +266,16 @@ pub(crate) async fn add( } } + // Enable the default groups of the project + defaulted_groups = + groups.with_defaults(default_dependency_groups(project.pyproject_toml())?); + if frozen || no_sync { // Discover the interpreter. let interpreter = ProjectInterpreter::discover( project.workspace(), project_dir, + &defaulted_groups, python.as_deref().map(PythonRequest::parse), &network_settings, python_preference, @@ -258,6 +295,7 @@ pub(crate) async fn add( // Discover or create the virtual environment. let environment = ProjectEnvironment::get_or_init( project.workspace(), + &defaulted_groups, python.as_deref().map(PythonRequest::parse), &install_mirrors, &network_settings, @@ -468,7 +506,7 @@ pub(crate) async fn add( rev.as_deref(), tag.as_deref(), branch.as_deref(), - &extras, + &extras_of_dependency, index, &mut toml, )?; @@ -551,7 +589,8 @@ pub(crate) async fn add( lock_state, sync_state, locked, - &dependency_type, + &defaulted_extras, + &defaulted_groups, raw, bounds, constraints, @@ -778,7 +817,8 @@ async fn lock_and_sync( lock_state: UniversalState, sync_state: PlatformState, locked: bool, - dependency_type: &DependencyType, + extras: &ExtrasSpecificationWithDefaults, + groups: &DependencyGroupsWithDefaults, raw: bool, bound_kind: Option, constraints: Vec, @@ -942,36 +982,6 @@ async fn lock_and_sync( return Ok(()); }; - // Sync the environment. - let (extras, dev) = match dependency_type { - DependencyType::Production => { - let extras = ExtrasSpecification::from_extra(vec![]); - let dev = DependencyGroups::from_dev_mode(DevMode::Exclude); - (extras, dev) - } - DependencyType::Dev => { - let extras = ExtrasSpecification::from_extra(vec![]); - let dev = DependencyGroups::from_dev_mode(DevMode::Include); - (extras, dev) - } - DependencyType::Optional(extra_name) => { - let extras = ExtrasSpecification::from_extra(vec![extra_name.clone()]); - let dev = DependencyGroups::from_dev_mode(DevMode::Exclude); - (extras, dev) - } - DependencyType::Group(group_name) => { - let extras = ExtrasSpecification::from_extra(vec![]); - let dev = DependencyGroups::from_group(group_name.clone()); - (extras, dev) - } - }; - - // Determine the default groups to include. - let default_groups = default_dependency_groups(project.pyproject_toml())?; - - // Determine the default extras to include. - let default_extras = DefaultExtras::default(); - // Identify the installation target. let target = match &project { VirtualProject::Project(project) => InstallTarget::Project { @@ -988,8 +998,8 @@ async fn lock_and_sync( project::sync::do_sync( target, venv, - &extras.with_defaults(default_extras), - &dev.with_defaults(default_groups), + extras, + groups, EditableMode::Editable, InstallOptions::default(), Modifications::Sufficient, diff --git a/crates/uv/src/commands/project/export.rs b/crates/uv/src/commands/project/export.rs index 566f4af41..ac228989c 100644 --- a/crates/uv/src/commands/project/export.rs +++ b/crates/uv/src/commands/project/export.rs @@ -61,7 +61,7 @@ pub(crate) async fn export( install_options: InstallOptions, output_file: Option, extras: ExtrasSpecification, - dev: DependencyGroups, + groups: DependencyGroups, editable: EditableMode, locked: bool, frozen: bool, @@ -122,7 +122,7 @@ pub(crate) async fn export( ExportTarget::Script(_) => DefaultExtras::default(), }; - let dev = dev.with_defaults(default_groups); + let groups = groups.with_defaults(default_groups); let extras = extras.with_defaults(default_extras); // Find an interpreter for the project, unless `--frozen` is set. @@ -148,6 +148,7 @@ pub(crate) async fn export( ExportTarget::Project(project) => ProjectInterpreter::discover( project.workspace(), project_dir, + &groups, python.as_deref().map(PythonRequest::parse), &network_settings, python_preference, @@ -206,7 +207,7 @@ pub(crate) async fn export( }; // Validate that the set of requested extras and development groups are compatible. - detect_conflicts(&lock, &extras, &dev)?; + detect_conflicts(&lock, &extras, &groups)?; // Identify the installation target. let target = match &target { @@ -259,7 +260,7 @@ pub(crate) async fn export( // Validate that the set of requested extras and development groups are defined in the lockfile. target.validate_extras(&extras)?; - target.validate_groups(&dev)?; + target.validate_groups(&groups)?; // Write the resolved dependencies to the output channel. let mut writer = OutputWriter::new(!quiet || output_file.is_none(), output_file.as_deref()); @@ -306,7 +307,7 @@ pub(crate) async fn export( &target, &prune, &extras, - &dev, + &groups, include_annotations, editable, hashes, @@ -328,7 +329,7 @@ pub(crate) async fn export( &target, &prune, &extras, - &dev, + &groups, include_annotations, editable, &install_options, diff --git a/crates/uv/src/commands/project/init.rs b/crates/uv/src/commands/project/init.rs index 376e8e007..71aacdc1b 100644 --- a/crates/uv/src/commands/project/init.rs +++ b/crates/uv/src/commands/project/init.rs @@ -4,13 +4,15 @@ use std::fmt::Write; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; use std::str::FromStr; +use uv_distribution_types::RequiresPython; use tracing::{debug, trace, warn}; use uv_cache::Cache; use uv_cli::AuthorFrom; use uv_client::BaseClientBuilder; use uv_configuration::{ - PreviewMode, ProjectBuildBackend, VersionControlError, VersionControlSystem, + DependencyGroupsWithDefaults, PreviewMode, ProjectBuildBackend, VersionControlError, + VersionControlSystem, }; use uv_fs::{CWD, Simplified}; use uv_git::GIT; @@ -21,7 +23,6 @@ use uv_python::{ PythonPreference, PythonRequest, PythonVariant, PythonVersionFile, VersionFileDiscoveryOptions, VersionRequest, }; -use uv_resolver::RequiresPython; use uv_scripts::{Pep723Script, ScriptTag}; use uv_settings::PythonInstallMirrors; use uv_static::EnvVars; @@ -502,7 +503,7 @@ async fn init_project( (requires_python, python_request) } else if let Some(requires_python) = workspace .as_ref() - .map(find_requires_python) + .map(|workspace| find_requires_python(workspace, &DependencyGroupsWithDefaults::none())) .transpose()? .flatten() { diff --git a/crates/uv/src/commands/project/install_target.rs b/crates/uv/src/commands/project/install_target.rs index d225114c9..b0f20e76f 100644 --- a/crates/uv/src/commands/project/install_target.rs +++ b/crates/uv/src/commands/project/install_target.rs @@ -165,11 +165,18 @@ impl<'lock> InstallTarget<'lock> { .requirements() .into_iter() .map(Cow::Owned) - .chain(workspace.dependency_groups().ok().into_iter().flat_map( - |dependency_groups| { - dependency_groups.into_values().flatten().map(Cow::Owned) - }, - )) + .chain( + workspace + .workspace_dependency_groups() + .ok() + .into_iter() + .flat_map(|dependency_groups| { + dependency_groups + .into_values() + .flat_map(|group| group.requirements) + .map(Cow::Owned) + }), + ) .chain(workspace.packages().values().flat_map(|member| { // Iterate over all dependencies in each member. let dependencies = member @@ -316,9 +323,15 @@ impl<'lock> InstallTarget<'lock> { let known_groups = member_packages .iter() .flat_map(|package| package.dependency_groups().keys().map(Cow::Borrowed)) - .chain(workspace.dependency_groups().ok().into_iter().flat_map( - |dependency_groups| dependency_groups.into_keys().map(Cow::Owned), - )) + .chain( + workspace + .workspace_dependency_groups() + .ok() + .into_iter() + .flat_map(|dependency_groups| { + dependency_groups.into_keys().map(Cow::Owned) + }), + ) .collect::>(); for group in groups.explicit_names() { diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index 89b3713cc..b57df429b 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -12,13 +12,14 @@ use tracing::debug; use uv_cache::Cache; use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ - Concurrency, Constraints, DryRun, ExtrasSpecification, PreviewMode, Reinstall, Upgrade, + Concurrency, Constraints, DependencyGroupsWithDefaults, DryRun, ExtrasSpecification, + PreviewMode, Reinstall, Upgrade, }; use uv_dispatch::BuildDispatch; use uv_distribution::DistributionDatabase; use uv_distribution_types::{ DependencyMetadata, HashGeneration, Index, IndexLocations, NameRequirementSpecification, - Requirement, UnresolvedRequirementSpecification, + Requirement, RequiresPython, UnresolvedRequirementSpecification, }; use uv_git::ResolvedRepositoryReference; use uv_normalize::{GroupName, PackageName}; @@ -28,7 +29,7 @@ use uv_python::{Interpreter, PythonDownloads, PythonEnvironment, PythonPreferenc use uv_requirements::ExtrasResolver; use uv_requirements::upgrade::{LockedRequirements, read_lock_requirements}; use uv_resolver::{ - FlatIndex, InMemoryIndex, Lock, Options, OptionsBuilder, PythonRequirement, RequiresPython, + FlatIndex, InMemoryIndex, Lock, Options, OptionsBuilder, PythonRequirement, ResolverEnvironment, ResolverManifest, SatisfiesResult, UniversalMarker, }; use uv_scripts::{Pep723ItemRef, Pep723Script}; @@ -142,6 +143,8 @@ pub(crate) async fn lock( LockTarget::Workspace(workspace) => ProjectInterpreter::discover( workspace, project_dir, + // Don't enable any groups' requires-python for interpreter discovery + &DependencyGroupsWithDefaults::none(), python.as_deref().map(PythonRequest::parse), &network_settings, python_preference, @@ -437,8 +440,8 @@ async fn do_lock( let build_constraints = target.lower(build_constraints, index_locations, *sources)?; let dependency_groups = dependency_groups .into_iter() - .map(|(name, requirements)| { - let requirements = target.lower(requirements, index_locations, *sources)?; + .map(|(name, group)| { + let requirements = target.lower(group.requirements, index_locations, *sources)?; Ok((name, requirements)) }) .collect::, ProjectError>>()?; diff --git a/crates/uv/src/commands/project/lock_target.rs b/crates/uv/src/commands/project/lock_target.rs index cb45aa8ec..4618b3b84 100644 --- a/crates/uv/src/commands/project/lock_target.rs +++ b/crates/uv/src/commands/project/lock_target.rs @@ -3,15 +3,15 @@ use std::path::{Path, PathBuf}; use itertools::Either; -use uv_configuration::SourceStrategy; +use uv_configuration::{DependencyGroupsWithDefaults, SourceStrategy}; use uv_distribution::LoweredRequirement; -use uv_distribution_types::{Index, IndexLocations, Requirement}; +use uv_distribution_types::{Index, IndexLocations, Requirement, RequiresPython}; use uv_normalize::{GroupName, PackageName}; use uv_pep508::RequirementOrigin; use uv_pypi_types::{Conflicts, SupportedEnvironments, VerbatimParsedUrl}; -use uv_resolver::{Lock, LockVersion, RequiresPython, VERSION}; +use uv_resolver::{Lock, LockVersion, VERSION}; use uv_scripts::Pep723Script; -use uv_workspace::dependency_groups::DependencyGroupError; +use uv_workspace::dependency_groups::{DependencyGroupError, FlatDependencyGroup}; use uv_workspace::{Workspace, WorkspaceMember}; use crate::commands::project::{ProjectError, find_requires_python}; @@ -100,12 +100,9 @@ impl<'lock> LockTarget<'lock> { /// attached to any members within the target. pub(crate) fn dependency_groups( self, - ) -> Result< - BTreeMap>>, - DependencyGroupError, - > { + ) -> Result, DependencyGroupError> { match self { - Self::Workspace(workspace) => workspace.dependency_groups(), + Self::Workspace(workspace) => workspace.workspace_dependency_groups(), Self::Script(_) => Ok(BTreeMap::new()), } } @@ -219,7 +216,11 @@ impl<'lock> LockTarget<'lock> { #[allow(clippy::result_large_err)] pub(crate) fn requires_python(self) -> Result, ProjectError> { match self { - Self::Workspace(workspace) => find_requires_python(workspace), + Self::Workspace(workspace) => { + // When locking, don't try to enforce requires-python bounds that appear on groups + let groups = DependencyGroupsWithDefaults::none(); + find_requires_python(workspace, &groups) + } Self::Script(script) => Ok(script .metadata .requires_python diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index d2efc3ccd..85defd4dd 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -18,7 +18,8 @@ use uv_configuration::{ use uv_dispatch::{BuildDispatch, SharedState}; use uv_distribution::{DistributionDatabase, LoweredRequirement}; use uv_distribution_types::{ - Index, Requirement, Resolution, UnresolvedRequirement, UnresolvedRequirementSpecification, + Index, Requirement, RequiresPython, Resolution, UnresolvedRequirement, + UnresolvedRequirementSpecification, }; use uv_fs::{CWD, LockedFile, Simplified}; use uv_git::ResolvedRepositoryReference; @@ -35,8 +36,8 @@ use uv_python::{ use uv_requirements::upgrade::{LockedRequirements, read_lock_requirements}; use uv_requirements::{NamedRequirementsResolver, RequirementsSpecification}; use uv_resolver::{ - FlatIndex, Lock, OptionsBuilder, Preference, PythonRequirement, RequiresPython, - ResolverEnvironment, ResolverOutput, + FlatIndex, Lock, OptionsBuilder, Preference, PythonRequirement, ResolverEnvironment, + ResolverOutput, }; use uv_scripts::Pep723ItemRef; use uv_settings::PythonInstallMirrors; @@ -45,7 +46,7 @@ use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy}; use uv_warnings::{warn_user, warn_user_once}; use uv_workspace::dependency_groups::DependencyGroupError; use uv_workspace::pyproject::PyProjectToml; -use uv_workspace::{Workspace, WorkspaceCache}; +use uv_workspace::{RequiresPythonSources, Workspace, WorkspaceCache}; use crate::commands::pip::loggers::{InstallLogger, ResolveLogger}; use crate::commands::pip::operations::{Changelog, Modifications}; @@ -108,19 +109,28 @@ pub(crate) enum ProjectError { Conflict(#[from] ConflictError), #[error( - "The requested interpreter resolved to Python {0}, which is incompatible with the project's Python requirement: `{1}`" + "The requested interpreter resolved to Python {_0}, which is incompatible with the project's Python requirement: `{_1}`{}", + format_optional_requires_python_sources(_2, *_3) )] - RequestedPythonProjectIncompatibility(Version, RequiresPython), + RequestedPythonProjectIncompatibility(Version, RequiresPython, RequiresPythonSources, bool), #[error( - "The Python request from `{0}` resolved to Python {1}, which is incompatible with the project's Python requirement: `{2}`. Use `uv python pin` to update the `.python-version` file to a compatible version." + "The Python request from `{_0}` resolved to Python {_1}, which is incompatible with the project's Python requirement: `{_2}`{}\nUse `uv python pin` to update the `.python-version` file to a compatible version", + format_optional_requires_python_sources(_3, *_4) )] - DotPythonVersionProjectIncompatibility(String, Version, RequiresPython), + DotPythonVersionProjectIncompatibility( + String, + Version, + RequiresPython, + RequiresPythonSources, + bool, + ), #[error( - "The resolved Python interpreter (Python {0}) is incompatible with the project's Python requirement: `{1}`" + "The resolved Python interpreter (Python {_0}) is incompatible with the project's Python requirement: `{_1}`{}", + format_optional_requires_python_sources(_2, *_3) )] - RequiresPythonProjectIncompatibility(Version, RequiresPython), + RequiresPythonProjectIncompatibility(Version, RequiresPython, RequiresPythonSources, bool), #[error( "The requested interpreter resolved to Python {0}, which is incompatible with the script's Python requirement: `{1}`" @@ -137,34 +147,6 @@ pub(crate) enum ProjectError { )] RequiresPythonScriptIncompatibility(Version, RequiresPython), - #[error("The requested interpreter resolved to Python {0}, which is incompatible with the project's Python requirement: `{1}`. However, a workspace member (`{member}`) supports Python {3}. To install the workspace member on its own, navigate to `{path}`, then run `{venv}` followed by `{install}`.", member = _2.cyan(), venv = format!("uv venv --python {_0}").green(), install = "uv pip install -e .".green(), path = _4.user_display().cyan() )] - RequestedMemberIncompatibility( - Version, - RequiresPython, - PackageName, - VersionSpecifiers, - PathBuf, - ), - - #[error("The Python request from `{0}` resolved to Python {1}, which is incompatible with the project's Python requirement: `{2}`. However, a workspace member (`{member}`) supports Python {4}. To install the workspace member on its own, navigate to `{path}`, then run `{venv}` followed by `{install}`.", member = _3.cyan(), venv = format!("uv venv --python {_1}").green(), install = "uv pip install -e .".green(), path = _5.user_display().cyan() )] - DotPythonVersionMemberIncompatibility( - String, - Version, - RequiresPython, - PackageName, - VersionSpecifiers, - PathBuf, - ), - - #[error("The resolved Python interpreter (Python {0}) is incompatible with the project's Python requirement: `{1}`. However, a workspace member (`{member}`) supports Python {3}. To install the workspace member on its own, navigate to `{path}`, then run `{venv}` followed by `{install}`.", member = _2.cyan(), venv = format!("uv venv --python {_0}").green(), install = "uv pip install -e .".green(), path = _4.user_display().cyan() )] - RequiresPythonMemberIncompatibility( - Version, - RequiresPython, - PackageName, - VersionSpecifiers, - PathBuf, - ), - #[error("Group `{0}` is not defined in the project's `dependency-groups` table")] MissingGroupProject(GroupName), @@ -194,8 +176,11 @@ pub(crate) enum ProjectError { #[error("Environment markers `{0}` don't overlap with Python requirement `{1}`")] DisjointEnvironment(MarkerTreeContents, VersionSpecifiers), - #[error("The workspace contains conflicting Python requirements:\n{}", _0.iter().map(|(name, specifiers)| format!("- `{name}`: `{specifiers}`")).join("\n"))] - DisjointRequiresPython(BTreeMap), + #[error( + "Found conflicting Python requirements:\n{}", + format_requires_python_sources(_0) + )] + DisjointRequiresPython(BTreeMap<(PackageName, Option), VersionSpecifiers>), #[error("Environment marker is empty")] EmptyEnvironment, @@ -286,7 +271,7 @@ pub(crate) struct ConflictError { /// The items from the set that were enabled, and thus create the conflict. pub(crate) conflicts: Vec, /// Enabled dependency groups with defaults applied. - pub(crate) dev: DependencyGroupsWithDefaults, + pub(crate) groups: DependencyGroupsWithDefaults, } impl std::fmt::Display for ConflictError { @@ -338,7 +323,7 @@ impl std::fmt::Display for ConflictError { .iter() .map(|conflict| match conflict { ConflictPackage::Group(group) - if self.dev.contains_because_default(group) => + if self.groups.contains_because_default(group) => format!("`{group}` (enabled by default)"), ConflictPackage::Group(group) => format!("`{group}`"), ConflictPackage::Extra(..) => unreachable!(), @@ -358,7 +343,7 @@ impl std::fmt::Display for ConflictError { let conflict = match conflict { ConflictPackage::Extra(extra) => format!("extra `{extra}`"), ConflictPackage::Group(group) - if self.dev.contains_because_default(group) => + if self.groups.contains_because_default(group) => { format!("group `{group}` (enabled by default)") } @@ -429,23 +414,16 @@ impl PlatformState { #[allow(clippy::result_large_err)] pub(crate) fn find_requires_python( workspace: &Workspace, + groups: &DependencyGroupsWithDefaults, ) -> Result, ProjectError> { + let requires_python = workspace.requires_python(groups)?; // If there are no `Requires-Python` specifiers in the workspace, return `None`. - if workspace.requires_python().next().is_none() { + if requires_python.is_empty() { return Ok(None); } - match RequiresPython::intersection( - workspace - .requires_python() - .map(|(.., specifiers)| specifiers), - ) { + match RequiresPython::intersection(requires_python.iter().map(|(.., specifiers)| specifiers)) { Some(requires_python) => Ok(Some(requires_python)), - None => Err(ProjectError::DisjointRequiresPython( - workspace - .requires_python() - .map(|(name, specifiers)| (name.clone(), specifiers.clone())) - .collect(), - )), + None => Err(ProjectError::DisjointRequiresPython(requires_python)), } } @@ -457,6 +435,7 @@ pub(crate) fn find_requires_python( pub(crate) fn validate_project_requires_python( interpreter: &Interpreter, workspace: Option<&Workspace>, + groups: &DependencyGroupsWithDefaults, requires_python: &RequiresPython, source: &PythonRequestSource, ) -> Result<(), ProjectError> { @@ -464,57 +443,24 @@ pub(crate) fn validate_project_requires_python( return Ok(()); } - // If the Python version is compatible with one of the workspace _members_, raise - // a dedicated error. For example, if the workspace root requires Python >=3.12, but - // a library in the workspace is compatible with Python >=3.8, the user may attempt - // to sync on Python 3.8. This will fail, but we should provide a more helpful error - // message. - for (name, member) in workspace.into_iter().flat_map(Workspace::packages) { - let Some(project) = member.pyproject_toml().project.as_ref() else { - continue; - }; - let Some(specifiers) = project.requires_python.as_ref() else { - continue; - }; - if specifiers.contains(interpreter.python_version()) { - return match source { - PythonRequestSource::UserRequest => { - Err(ProjectError::RequestedMemberIncompatibility( - interpreter.python_version().clone(), - requires_python.clone(), - name.clone(), - specifiers.clone(), - member.root().clone(), - )) - } - PythonRequestSource::DotPythonVersion(file) => { - Err(ProjectError::DotPythonVersionMemberIncompatibility( - file.path().user_display().to_string(), - interpreter.python_version().clone(), - requires_python.clone(), - name.clone(), - specifiers.clone(), - member.root().clone(), - )) - } - PythonRequestSource::RequiresPython => { - Err(ProjectError::RequiresPythonMemberIncompatibility( - interpreter.python_version().clone(), - requires_python.clone(), - name.clone(), - specifiers.clone(), - member.root().clone(), - )) - } - }; - } - } + // Find all the individual requires_python constraints that conflict + let conflicting_requires = workspace + .and_then(|workspace| workspace.requires_python(groups).ok()) + .into_iter() + .flatten() + .filter(|(.., requires)| !requires.contains(interpreter.python_version())) + .collect::(); + let workspace_non_trivial = workspace + .map(|workspace| workspace.packages().len() > 1) + .unwrap_or(false); match source { PythonRequestSource::UserRequest => { Err(ProjectError::RequestedPythonProjectIncompatibility( interpreter.python_version().clone(), requires_python.clone(), + conflicting_requires, + workspace_non_trivial, )) } PythonRequestSource::DotPythonVersion(file) => { @@ -522,12 +468,16 @@ pub(crate) fn validate_project_requires_python( file.path().user_display().to_string(), interpreter.python_version().clone(), requires_python.clone(), + conflicting_requires, + workspace_non_trivial, )) } PythonRequestSource::RequiresPython => { Err(ProjectError::RequiresPythonProjectIncompatibility( interpreter.python_version().clone(), requires_python.clone(), + conflicting_requires, + workspace_non_trivial, )) } } @@ -738,7 +688,13 @@ impl ScriptInterpreter { if let Err(err) = match requires_python { Some((requires_python, RequiresPythonSource::Project)) => { - validate_project_requires_python(&interpreter, workspace, &requires_python, &source) + validate_project_requires_python( + &interpreter, + workspace, + &DependencyGroupsWithDefaults::none(), + &requires_python, + &source, + ) } Some((requires_python, RequiresPythonSource::Script)) => { validate_script_requires_python(&interpreter, &requires_python, &source) @@ -874,6 +830,7 @@ impl ProjectInterpreter { pub(crate) async fn discover( workspace: &Workspace, project_dir: &Path, + groups: &DependencyGroupsWithDefaults, python_request: Option, network_settings: &NetworkSettings, python_preference: PythonPreference, @@ -890,8 +847,14 @@ impl ProjectInterpreter { source, python_request, requires_python, - } = WorkspacePython::from_request(python_request, Some(workspace), project_dir, no_config) - .await?; + } = WorkspacePython::from_request( + python_request, + Some(workspace), + groups, + project_dir, + no_config, + ) + .await?; // Read from the virtual environment first. let root = workspace.venv(active); @@ -1002,6 +965,7 @@ impl ProjectInterpreter { validate_project_requires_python( &interpreter, Some(workspace), + groups, requires_python, &source, )?; @@ -1081,10 +1045,14 @@ impl WorkspacePython { pub(crate) async fn from_request( python_request: Option, workspace: Option<&Workspace>, + groups: &DependencyGroupsWithDefaults, project_dir: &Path, no_config: bool, ) -> Result { - let requires_python = workspace.map(find_requires_python).transpose()?.flatten(); + let requires_python = workspace + .map(|workspace| find_requires_python(workspace, groups)) + .transpose()? + .flatten(); let workspace_root = workspace.map(Workspace::install_path); @@ -1165,6 +1133,8 @@ impl ScriptPython { } = WorkspacePython::from_request( python_request, workspace, + // Scripts have no groups to hang requires-python settings off of + &DependencyGroupsWithDefaults::none(), script.path().and_then(Path::parent).unwrap_or(&**CWD), no_config, ) @@ -1231,6 +1201,7 @@ impl ProjectEnvironment { /// Initialize a virtual environment for the current project. pub(crate) async fn get_or_init( workspace: &Workspace, + groups: &DependencyGroupsWithDefaults, python: Option, install_mirrors: &PythonInstallMirrors, network_settings: &NetworkSettings, @@ -1249,6 +1220,7 @@ impl ProjectEnvironment { match ProjectInterpreter::discover( workspace, workspace.install_path().as_ref(), + groups, python, network_settings, python_preference, @@ -2434,7 +2406,7 @@ pub(crate) fn default_dependency_groups( pub(crate) fn detect_conflicts( lock: &Lock, extras: &ExtrasSpecification, - dev: &DependencyGroupsWithDefaults, + groups: &DependencyGroupsWithDefaults, ) -> Result<(), ProjectError> { // Note that we need to collect all extras and groups that match in // a particular set, since extras can be declared as conflicting with @@ -2453,7 +2425,7 @@ pub(crate) fn detect_conflicts( } if item .group() - .map(|group| dev.contains(group)) + .map(|group| groups.contains(group)) .unwrap_or(false) { conflicts.push(item.conflict().clone()); @@ -2463,7 +2435,7 @@ pub(crate) fn detect_conflicts( return Err(ProjectError::Conflict(ConflictError { set: set.clone(), conflicts, - dev: dev.clone(), + groups: groups.clone(), })); } } @@ -2677,6 +2649,50 @@ fn cache_name(name: &str) -> Option> { } } +fn format_requires_python_sources(conflicts: &RequiresPythonSources) -> String { + conflicts + .iter() + .map(|((package, group), specifiers)| { + if let Some(group) = group { + format!("- {package}:{group}: {specifiers}") + } else { + format!("- {package}: {specifiers}") + } + }) + .join("\n") +} + +fn format_optional_requires_python_sources( + conflicts: &RequiresPythonSources, + workspace_non_trivial: bool, +) -> String { + // If there's lots of conflicts, print a list + if conflicts.len() > 1 { + return format!( + ".\nThe following `requires-python` declarations do not permit this version:\n{}", + format_requires_python_sources(conflicts) + ); + } + // If there's one conflict, give a clean message + if conflicts.len() == 1 { + let ((package, group), _) = conflicts.iter().next().unwrap(); + if let Some(group) = group { + if workspace_non_trivial { + return format!( + " (from workspace member `{package}`'s `tool.uv.dependency-groups.{group}.requires-python`)." + ); + } + return format!(" (from `tool.uv.dependency-groups.{group}.requires-python`)."); + } + if workspace_non_trivial { + return format!(" (from workspace member `{package}`'s `project.requires-python`)."); + } + return " (from `project.requires-python`)".to_owned(); + } + // Otherwise don't elaborate + String::new() +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/uv/src/commands/project/remove.rs b/crates/uv/src/commands/project/remove.rs index 6dab60012..d17cd88ed 100644 --- a/crates/uv/src/commands/project/remove.rs +++ b/crates/uv/src/commands/project/remove.rs @@ -13,7 +13,7 @@ use uv_configuration::{ PreviewMode, }; use uv_fs::Simplified; -use uv_normalize::{DEV_DEPENDENCIES, DefaultExtras}; +use uv_normalize::{DEV_DEPENDENCIES, DefaultExtras, DefaultGroups}; use uv_pep508::PackageName; use uv_python::{PythonDownloads, PythonPreference, PythonRequest}; use uv_scripts::{Pep723ItemRef, Pep723Metadata, Pep723Script}; @@ -202,6 +202,14 @@ pub(crate) async fn remove( // Update the `pypackage.toml` in-memory. let target = target.update(&content)?; + // Determine enabled groups and extras + let default_groups = match &target { + RemoveTarget::Project(project) => default_dependency_groups(project.pyproject_toml())?, + RemoveTarget::Script(_) => DefaultGroups::default(), + }; + let groups = DependencyGroups::default().with_defaults(default_groups); + let extras = ExtrasSpecification::default().with_defaults(DefaultExtras::default()); + // Convert to an `AddTarget` by attaching the appropriate interpreter or environment. let target = match target { RemoveTarget::Project(project) => { @@ -210,6 +218,7 @@ pub(crate) async fn remove( let interpreter = ProjectInterpreter::discover( project.workspace(), project_dir, + &groups, python.as_deref().map(PythonRequest::parse), &network_settings, python_preference, @@ -229,6 +238,7 @@ pub(crate) async fn remove( // Discover or create the virtual environment. let environment = ProjectEnvironment::get_or_init( project.workspace(), + &groups, python.as_deref().map(PythonRequest::parse), &install_mirrors, &network_settings, @@ -314,12 +324,6 @@ pub(crate) async fn remove( return Ok(ExitStatus::Success); }; - // Determine the default groups to include. - let default_groups = default_dependency_groups(project.pyproject_toml())?; - - // Determine the default extras to include. - let default_extras = DefaultExtras::default(); - // Identify the installation target. let target = match &project { VirtualProject::Project(project) => InstallTarget::Project { @@ -338,8 +342,8 @@ pub(crate) async fn remove( match project::sync::do_sync( target, venv, - &ExtrasSpecification::default().with_defaults(default_extras), - &DependencyGroups::default().with_defaults(default_groups), + &extras, + &groups, EditableMode::Editable, InstallOptions::default(), Modifications::Exact, diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index 035528b1c..f97ffdbc1 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -78,7 +78,7 @@ pub(crate) async fn run( no_project: bool, no_config: bool, extras: ExtrasSpecification, - dev: DependencyGroups, + groups: DependencyGroups, editable: EditableMode, modifications: Modifications, python: Option, @@ -291,7 +291,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl target, &environment, &extras.with_defaults(DefaultExtras::default()), - &dev.with_defaults(DefaultGroups::default()), + &groups.with_defaults(DefaultGroups::default()), editable, install_options, modifications, @@ -468,7 +468,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl if !extras.is_empty() { warn_user!("Extras are not supported for Python scripts with inline metadata"); } - for flag in dev.history().as_flags_pretty() { + for flag in groups.history().as_flags_pretty() { warn_user!("`{flag}` is not supported for Python scripts with inline metadata"); } if all_packages { @@ -543,7 +543,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl for flag in extras.history().as_flags_pretty() { warn_user!("`{flag}` has no effect when used alongside `--no-project`"); } - for flag in dev.history().as_flags_pretty() { + for flag in groups.history().as_flags_pretty() { warn_user!("`{flag}` has no effect when used alongside `--no-project`"); } if locked { @@ -560,7 +560,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl for flag in extras.history().as_flags_pretty() { warn_user!("`{flag}` has no effect when used outside of a project"); } - for flag in dev.history().as_flags_pretty() { + for flag in groups.history().as_flags_pretty() { warn_user!("`{flag}` has no effect when used outside of a project"); } if locked { @@ -583,6 +583,11 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl project.workspace().install_path().display() ); } + // Determine the groups and extras to include. + let default_groups = default_dependency_groups(project.pyproject_toml())?; + let default_extras = DefaultExtras::default(); + let groups = groups.with_defaults(default_groups); + let extras = extras.with_defaults(default_extras); let venv = if isolated { debug!("Creating isolated virtual environment"); @@ -602,6 +607,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl } = WorkspacePython::from_request( python.as_deref().map(PythonRequest::parse), Some(project.workspace()), + &groups, project_dir, no_config, ) @@ -626,6 +632,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl validate_project_requires_python( &interpreter, Some(project.workspace()), + &groups, requires_python, &source, )?; @@ -647,6 +654,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl // project. ProjectEnvironment::get_or_init( project.workspace(), + &groups, python.as_deref().map(PythonRequest::parse), &install_mirrors, &network_settings, @@ -677,14 +685,6 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl .map(|lock| (lock, project.workspace().install_path().to_owned())); } } else { - // Validate that any referenced dependency groups are defined in the workspace. - - // Determine the default groups to include. - let default_groups = default_dependency_groups(project.pyproject_toml())?; - - // Determine the default extras to include. - let default_extras = DefaultExtras::default(); - // Determine the lock mode. let mode = if frozen { LockMode::Frozen @@ -769,18 +769,15 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl }; let install_options = InstallOptions::default(); - let dev = dev.with_defaults(default_groups); - let extras = extras.with_defaults(default_extras); - // Validate that the set of requested extras and development groups are defined in the lockfile. target.validate_extras(&extras)?; - target.validate_groups(&dev)?; + target.validate_groups(&groups)?; match project::sync::do_sync( target, &venv, &extras, - &dev, + &groups, editable, install_options, modifications, diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index ed96795e5..940b3a653 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -57,7 +57,7 @@ pub(crate) async fn sync( all_packages: bool, package: Option, extras: ExtrasSpecification, - dev: DependencyGroups, + groups: DependencyGroups, editable: EditableMode, install_options: InstallOptions, modifications: Modifications, @@ -116,23 +116,24 @@ pub(crate) async fn sync( SyncTarget::Project(project) }; - // Determine the default groups to include. + // Determine the groups and extras to include. let default_groups = match &target { SyncTarget::Project(project) => default_dependency_groups(project.pyproject_toml())?, SyncTarget::Script(..) => DefaultGroups::default(), }; - - // Determine the default extras to include. let default_extras = match &target { SyncTarget::Project(_project) => DefaultExtras::default(), SyncTarget::Script(..) => DefaultExtras::default(), }; + let groups = groups.with_defaults(default_groups); + let extras = extras.with_defaults(default_extras); // Discover or create the virtual environment. let environment = match &target { SyncTarget::Project(project) => SyncEnvironment::Project( ProjectEnvironment::get_or_init( project.workspace(), + &groups, python.as_deref().map(PythonRequest::parse), &install_mirrors, &network_settings, @@ -437,8 +438,8 @@ pub(crate) async fn sync( match do_sync( sync_target, &environment, - &extras.with_defaults(default_extras), - &dev.with_defaults(default_groups), + &extras, + &groups, editable, install_options, modifications, @@ -573,7 +574,7 @@ pub(super) async fn do_sync( target: InstallTarget<'_>, venv: &PythonEnvironment, extras: &ExtrasSpecificationWithDefaults, - dev: &DependencyGroupsWithDefaults, + groups: &DependencyGroupsWithDefaults, editable: EditableMode, install_options: InstallOptions, modifications: Modifications, @@ -624,11 +625,11 @@ pub(super) async fn do_sync( } // Validate that the set of requested extras and development groups are compatible. - detect_conflicts(target.lock(), extras, dev)?; + detect_conflicts(target.lock(), extras, groups)?; // Validate that the set of requested extras and development groups are defined in the lockfile. target.validate_extras(extras)?; - target.validate_groups(dev)?; + target.validate_groups(groups)?; // Determine the markers to use for resolution. let marker_env = venv.interpreter().resolver_marker_environment(); @@ -665,7 +666,7 @@ pub(super) async fn do_sync( &marker_env, tags, extras, - dev, + groups, build_options, &install_options, )?; diff --git a/crates/uv/src/commands/project/tree.rs b/crates/uv/src/commands/project/tree.rs index 6bf57d1a7..9c42a8a86 100644 --- a/crates/uv/src/commands/project/tree.rs +++ b/crates/uv/src/commands/project/tree.rs @@ -34,7 +34,7 @@ use crate::settings::{NetworkSettings, ResolverSettings}; #[allow(clippy::fn_params_excessive_bools)] pub(crate) async fn tree( project_dir: &Path, - dev: DependencyGroups, + groups: DependencyGroups, locked: bool, frozen: bool, universal: bool, @@ -71,11 +71,12 @@ pub(crate) async fn tree( LockTarget::Workspace(&workspace) }; - // Determine the default groups to include. - let defaults = match target { + // Determine the groups to include. + let default_groups = match target { LockTarget::Workspace(workspace) => default_dependency_groups(workspace.pyproject_toml())?, LockTarget::Script(_) => DefaultGroups::default(), }; + let groups = groups.with_defaults(default_groups); let native_tls = network_settings.native_tls; @@ -102,6 +103,7 @@ pub(crate) async fn tree( LockTarget::Workspace(workspace) => ProjectInterpreter::discover( workspace, project_dir, + &groups, python.as_deref().map(PythonRequest::parse), network_settings, python_preference, @@ -271,7 +273,7 @@ pub(crate) async fn tree( depth.into(), &prune, &package, - &dev.with_defaults(defaults), + &groups, no_dedupe, invert, ); diff --git a/crates/uv/src/commands/project/version.rs b/crates/uv/src/commands/project/version.rs index 0e50c2ac0..f76744186 100644 --- a/crates/uv/src/commands/project/version.rs +++ b/crates/uv/src/commands/project/version.rs @@ -10,8 +10,8 @@ use uv_cache::Cache; use uv_cli::version::VersionInfo; use uv_cli::{VersionBump, VersionFormat}; use uv_configuration::{ - Concurrency, DependencyGroups, DryRun, EditableMode, ExtrasSpecification, InstallOptions, - PreviewMode, + Concurrency, DependencyGroups, DependencyGroupsWithDefaults, DryRun, EditableMode, + ExtrasSpecification, InstallOptions, PreviewMode, }; use uv_fs::Simplified; use uv_normalize::DefaultExtras; @@ -285,6 +285,7 @@ async fn print_frozen_version( let interpreter = ProjectInterpreter::discover( project.workspace(), project_dir, + &DependencyGroupsWithDefaults::none(), python.as_deref().map(PythonRequest::parse), &network_settings, python_preference, @@ -378,12 +379,20 @@ async fn lock_and_sync( return Ok(ExitStatus::Success); } + // Determine the groups and extras that should be enabled. + let default_groups = default_dependency_groups(project.pyproject_toml())?; + let default_extras = DefaultExtras::default(); + let groups = DependencyGroups::default().with_defaults(default_groups); + let extras = ExtrasSpecification::from_all_extras().with_defaults(default_extras); + let install_options = InstallOptions::default(); + // Convert to an `AddTarget` by attaching the appropriate interpreter or environment. let target = if no_sync { // Discover the interpreter. let interpreter = ProjectInterpreter::discover( project.workspace(), project_dir, + &groups, python.as_deref().map(PythonRequest::parse), &network_settings, python_preference, @@ -403,6 +412,7 @@ async fn lock_and_sync( // Discover or create the virtual environment. let environment = ProjectEnvironment::get_or_init( project.workspace(), + &groups, python.as_deref().map(PythonRequest::parse), &install_mirrors, &network_settings, @@ -466,15 +476,6 @@ async fn lock_and_sync( }; // Perform a full sync, because we don't know what exactly is affected by the version. - // TODO(ibraheem): Should we accept CLI overrides for this? Should we even sync here? - let extras = ExtrasSpecification::from_all_extras(); - let install_options = InstallOptions::default(); - - // Determine the default groups to include. - let default_groups = default_dependency_groups(project.pyproject_toml())?; - - // Determine the default extras to include. - let default_extras = DefaultExtras::default(); // Identify the installation target. let target = match &project { @@ -494,8 +495,8 @@ async fn lock_and_sync( match project::sync::do_sync( target, venv, - &extras.with_defaults(default_extras), - &DependencyGroups::default().with_defaults(default_groups), + &extras, + &groups, EditableMode::Editable, install_options, Modifications::Sufficient, diff --git a/crates/uv/src/commands/python/find.rs b/crates/uv/src/commands/python/find.rs index 63e25fed1..1e5693c65 100644 --- a/crates/uv/src/commands/python/find.rs +++ b/crates/uv/src/commands/python/find.rs @@ -1,6 +1,7 @@ use anyhow::Result; use std::fmt::Write; use std::path::Path; +use uv_configuration::DependencyGroupsWithDefaults; use uv_cache::Cache; use uv_fs::Simplified; @@ -56,6 +57,8 @@ pub(crate) async fn find( } }; + // Don't enable the requires-python settings on groups + let groups = DependencyGroupsWithDefaults::none(); let WorkspacePython { source, python_request, @@ -63,6 +66,7 @@ pub(crate) async fn find( } = WorkspacePython::from_request( request.map(|request| PythonRequest::parse(&request)), project.as_ref().map(VirtualProject::workspace), + &groups, project_dir, no_config, ) @@ -80,6 +84,7 @@ pub(crate) async fn find( match validate_project_requires_python( python.interpreter(), project.as_ref().map(VirtualProject::workspace), + &groups, &requires_python, &source, ) { diff --git a/crates/uv/src/commands/python/pin.rs b/crates/uv/src/commands/python/pin.rs index e0b241bcc..a0af7ec41 100644 --- a/crates/uv/src/commands/python/pin.rs +++ b/crates/uv/src/commands/python/pin.rs @@ -8,6 +8,7 @@ use tracing::debug; use uv_cache::Cache; use uv_client::BaseClientBuilder; +use uv_configuration::DependencyGroupsWithDefaults; use uv_dirs::user_uv_config_dir; use uv_fs::Simplified; use uv_python::{ @@ -322,6 +323,9 @@ struct Pin<'a> { /// Checks if the pinned Python version is compatible with the workspace/project's `Requires-Python`. fn assert_pin_compatible_with_project(pin: &Pin, virtual_project: &VirtualProject) -> Result<()> { + // Don't factor in requires-python settings on dependency-groups + let groups = DependencyGroupsWithDefaults::none(); + let (requires_python, project_type) = match virtual_project { VirtualProject::Project(project_workspace) => { debug!( @@ -329,7 +333,8 @@ fn assert_pin_compatible_with_project(pin: &Pin, virtual_project: &VirtualProjec project_workspace.project_name(), project_workspace.workspace().install_path().display() ); - let requires_python = find_requires_python(project_workspace.workspace())?; + + let requires_python = find_requires_python(project_workspace.workspace(), &groups)?; (requires_python, "project") } VirtualProject::NonProject(workspace) => { @@ -337,7 +342,7 @@ fn assert_pin_compatible_with_project(pin: &Pin, virtual_project: &VirtualProjec "Discovered virtual workspace at: {}", workspace.install_path().display() ); - let requires_python = find_requires_python(workspace)?; + let requires_python = find_requires_python(workspace, &groups)?; (requires_python, "workspace") } }; diff --git a/crates/uv/src/commands/venv.rs b/crates/uv/src/commands/venv.rs index c0cf03921..a50c0e155 100644 --- a/crates/uv/src/commands/venv.rs +++ b/crates/uv/src/commands/venv.rs @@ -13,14 +13,15 @@ use thiserror::Error; use uv_cache::Cache; use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ - BuildOptions, Concurrency, ConfigSettings, Constraints, IndexStrategy, KeyringProviderType, - NoBinary, NoBuild, PreviewMode, SourceStrategy, + BuildOptions, Concurrency, ConfigSettings, Constraints, DependencyGroups, IndexStrategy, + KeyringProviderType, NoBinary, NoBuild, PreviewMode, SourceStrategy, }; use uv_dispatch::{BuildDispatch, SharedState}; use uv_distribution_types::Requirement; use uv_distribution_types::{DependencyMetadata, Index, IndexLocations}; use uv_fs::Simplified; use uv_install_wheel::LinkMode; +use uv_normalize::DefaultGroups; use uv_python::{ EnvironmentPreference, PythonDownloads, PythonInstallation, PythonPreference, PythonRequest, }; @@ -39,6 +40,8 @@ use crate::commands::reporters::PythonDownloadReporter; use crate::printer::Printer; use crate::settings::NetworkSettings; +use super::project::default_dependency_groups; + /// Create a virtual environment. #[allow(clippy::unnecessary_wraps, clippy::fn_params_excessive_bools)] pub(crate) async fn venv( @@ -197,6 +200,13 @@ async fn venv_impl( let reporter = PythonDownloadReporter::single(printer); + // If the default dependency-groups demand a higher requires-python + // we should bias an empty venv to that to avoid churn. + let default_groups = match &project { + Some(project) => default_dependency_groups(project.pyproject_toml()).into_diagnostic()?, + None => DefaultGroups::default(), + }; + let groups = DependencyGroups::default().with_defaults(default_groups); let WorkspacePython { source, python_request, @@ -204,6 +214,7 @@ async fn venv_impl( } = WorkspacePython::from_request( python_request.map(PythonRequest::parse), project.as_ref().map(VirtualProject::workspace), + &groups, project_dir, no_config, ) @@ -246,6 +257,7 @@ async fn venv_impl( match validate_project_requires_python( &interpreter, project.as_ref().map(VirtualProject::workspace), + &groups, &requires_python, &source, ) { diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index b7b1a7859..51041bcbc 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -1704,7 +1704,7 @@ async fn run_project( args.no_project, no_config, args.extras, - args.dev, + args.groups, args.editable, args.modifications, args.python, @@ -1752,7 +1752,7 @@ async fn run_project( args.all_packages, args.package, args.extras, - args.dev, + args.groups, args.editable, args.install_options, args.modifications, @@ -2043,7 +2043,7 @@ async fn run_project( Box::pin(commands::tree( project_dir, - args.dev, + args.groups, args.locked, args.frozen, args.universal, @@ -2095,7 +2095,7 @@ async fn run_project( args.install_options, args.output_file, args.extras, - args.dev, + args.groups, args.editable, args.locked, args.frozen, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index b5eb2f5d0..f8d44b50c 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -313,7 +313,7 @@ pub(crate) struct RunSettings { pub(crate) locked: bool, pub(crate) frozen: bool, pub(crate) extras: ExtrasSpecification, - pub(crate) dev: DependencyGroups, + pub(crate) groups: DependencyGroups, pub(crate) editable: EditableMode, pub(crate) modifications: Modifications, pub(crate) with: Vec, @@ -404,7 +404,7 @@ impl RunSettings { vec![], flag(all_extras, no_all_extras).unwrap_or_default(), ), - dev: DependencyGroups::from_args( + groups: DependencyGroups::from_args( dev, no_dev, only_dev, @@ -1098,7 +1098,7 @@ pub(crate) struct SyncSettings { pub(crate) script: Option, pub(crate) active: Option, pub(crate) extras: ExtrasSpecification, - pub(crate) dev: DependencyGroups, + pub(crate) groups: DependencyGroups, pub(crate) editable: EditableMode, pub(crate) install_options: InstallOptions, pub(crate) modifications: Modifications, @@ -1180,7 +1180,7 @@ impl SyncSettings { vec![], flag(all_extras, no_all_extras).unwrap_or_default(), ), - dev: DependencyGroups::from_args( + groups: DependencyGroups::from_args( dev, no_dev, only_dev, @@ -1578,7 +1578,7 @@ impl VersionSettings { #[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone)] pub(crate) struct TreeSettings { - pub(crate) dev: DependencyGroups, + pub(crate) groups: DependencyGroups, pub(crate) locked: bool, pub(crate) frozen: bool, pub(crate) universal: bool, @@ -1626,7 +1626,7 @@ impl TreeSettings { .unwrap_or_default(); Self { - dev: DependencyGroups::from_args( + groups: DependencyGroups::from_args( dev, no_dev, only_dev, @@ -1664,7 +1664,7 @@ pub(crate) struct ExportSettings { pub(crate) package: Option, pub(crate) prune: Vec, pub(crate) extras: ExtrasSpecification, - pub(crate) dev: DependencyGroups, + pub(crate) groups: DependencyGroups, pub(crate) editable: EditableMode, pub(crate) hashes: bool, pub(crate) install_options: InstallOptions, @@ -1739,7 +1739,7 @@ impl ExportSettings { vec![], flag(all_extras, no_all_extras).unwrap_or_default(), ), - dev: DependencyGroups::from_args( + groups: DependencyGroups::from_args( dev, no_dev, only_dev, diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index 4387d348a..260211269 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -5259,16 +5259,16 @@ fn lock_requires_python_disjoint() -> Result<()> { "#, )?; - uv_snapshot!(context.filters(), context.lock(), @r###" + uv_snapshot!(context.filters(), context.lock(), @r" success: false exit_code: 2 ----- stdout ----- ----- stderr ----- - error: The workspace contains conflicting Python requirements: - - `child`: `==3.10` - - `project`: `>=3.12` - "###); + error: Found conflicting Python requirements: + - child: ==3.10 + - project: >=3.12 + "); Ok(()) } @@ -18298,29 +18298,30 @@ fn lock_request_requires_python() -> Result<()> { )?; // Request a version that conflicts with `--requires-python`. - uv_snapshot!(context.filters(), context.lock().arg("--python").arg("3.12"), @r###" + uv_snapshot!(context.filters(), context.lock().arg("--python").arg("3.12"), @r" success: false exit_code: 2 ----- stdout ----- ----- stderr ----- Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] - error: The requested interpreter resolved to Python 3.12.[X], which is incompatible with the project's Python requirement: `>=3.8, <=3.10` - "###); + error: The requested interpreter resolved to Python 3.12.[X], which is incompatible with the project's Python requirement: `>=3.8, <=3.10` (from `project.requires-python`) + "); // Add a `.python-version` file that conflicts. let python_version = context.temp_dir.child(".python-version"); python_version.write_str("3.12")?; - uv_snapshot!(context.filters(), context.lock(), @r###" + uv_snapshot!(context.filters(), context.lock(), @r" success: false exit_code: 2 ----- stdout ----- ----- stderr ----- Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] - error: The Python request from `.python-version` resolved to Python 3.12.[X], which is incompatible with the project's Python requirement: `>=3.8, <=3.10`. Use `uv python pin` to update the `.python-version` file to a compatible version. - "###); + error: The Python request from `.python-version` resolved to Python 3.12.[X], which is incompatible with the project's Python requirement: `>=3.8, <=3.10` (from `project.requires-python`) + Use `uv python pin` to update the `.python-version` file to a compatible version + "); Ok(()) } @@ -20660,6 +20661,465 @@ fn lock_group_include() -> Result<()> { Ok(()) } +#[test] +fn lock_group_requires_python() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["typing-extensions"] + + [dependency-groups] + foo = ["idna"] + bar = ["sortedcontainers", "sniffio"] + + [tool.uv.dependency-groups] + bar = { requires-python = ">=3.13" } + "#, + )?; + + uv_snapshot!(context.filters(), context.lock(), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 5 packages in [TIME] + "); + + let lock = context.read("uv.lock"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r#" + version = 1 + revision = 2 + requires-python = ">=3.12" + resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version < '3.13'", + ] + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [[package]] + name = "idna" + version = "3.6" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426, upload-time = "2023-11-25T15:40:54.902Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567, upload-time = "2023-11-25T15:40:52.604Z" }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "typing-extensions" }, + ] + + [package.dev-dependencies] + bar = [ + { name = "sniffio", marker = "python_full_version >= '3.13'" }, + { name = "sortedcontainers", marker = "python_full_version >= '3.13'" }, + ] + foo = [ + { name = "idna" }, + ] + + [package.metadata] + requires-dist = [{ name = "typing-extensions" }] + + [package.metadata.requires-dev] + bar = [ + { name = "sniffio", marker = "python_full_version >= '3.13'" }, + { name = "sortedcontainers", marker = "python_full_version >= '3.13'" }, + ] + foo = [{ name = "idna" }] + + [[package]] + name = "sniffio" + version = "1.3.1" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, + ] + + [[package]] + name = "sortedcontainers" + version = "2.4.0" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, + ] + + [[package]] + name = "typing-extensions" + version = "4.10.0" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/16/3a/0d26ce356c7465a19c9ea8814b960f8a36c3b0d07c323176620b7b483e44/typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb", size = 77558, upload-time = "2024-02-25T22:12:49.693Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/de/dc04a3ea60b22624b51c703a84bbe0184abcd1d0b9bc8074b5d6b7ab90bb/typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475", size = 33926, upload-time = "2024-02-25T22:12:47.72Z" }, + ] + "# + ); + }); + + Ok(()) +} + +#[test] +fn lock_group_includes_requires_python() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["typing-extensions"] + + [dependency-groups] + foo = ["idna", {include-group = "bar"}] + bar = ["sortedcontainers", "sniffio"] + baz = ["idna", {include-group = "bar"}] + blargh = ["idna", {include-group = "bar"}] + + [tool.uv.dependency-groups] + bar = { requires-python = ">=3.13" } + baz = { requires-python = ">=3.13.1" } + blargh = { requires-python = ">=3.12.1" } + "#, + )?; + + uv_snapshot!(context.filters(), context.lock(), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 5 packages in [TIME] + "); + + let lock = context.read("uv.lock"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r#" + version = 1 + revision = 2 + requires-python = ">=3.12" + resolution-markers = [ + "python_full_version >= '3.13.1'", + "python_full_version >= '3.13' and python_full_version < '3.13.1'", + "python_full_version >= '3.12.[X]' and python_full_version < '3.13'", + "python_full_version < '3.12.[X]'", + ] + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [[package]] + name = "idna" + version = "3.6" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426, upload-time = "2023-11-25T15:40:54.902Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567, upload-time = "2023-11-25T15:40:52.604Z" }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "typing-extensions" }, + ] + + [package.dev-dependencies] + bar = [ + { name = "sniffio", marker = "python_full_version >= '3.13'" }, + { name = "sortedcontainers", marker = "python_full_version >= '3.13'" }, + ] + baz = [ + { name = "idna", marker = "python_full_version >= '3.13.1'" }, + { name = "sniffio", marker = "python_full_version >= '3.13.1'" }, + { name = "sortedcontainers", marker = "python_full_version >= '3.13.1'" }, + ] + blargh = [ + { name = "idna", marker = "python_full_version >= '3.12.[X]'" }, + { name = "sniffio", marker = "python_full_version >= '3.13'" }, + { name = "sortedcontainers", marker = "python_full_version >= '3.13'" }, + ] + foo = [ + { name = "idna" }, + { name = "sniffio", marker = "python_full_version >= '3.13'" }, + { name = "sortedcontainers", marker = "python_full_version >= '3.13'" }, + ] + + [package.metadata] + requires-dist = [{ name = "typing-extensions" }] + + [package.metadata.requires-dev] + bar = [ + { name = "sniffio", marker = "python_full_version >= '3.13'" }, + { name = "sortedcontainers", marker = "python_full_version >= '3.13'" }, + ] + baz = [ + { name = "idna", marker = "python_full_version >= '3.13.1'" }, + { name = "sniffio", marker = "python_full_version >= '3.13.1'" }, + { name = "sortedcontainers", marker = "python_full_version >= '3.13.1'" }, + ] + blargh = [ + { name = "idna", marker = "python_full_version >= '3.12.[X]'" }, + { name = "sniffio", marker = "python_full_version >= '3.13'" }, + { name = "sortedcontainers", marker = "python_full_version >= '3.13'" }, + ] + foo = [ + { name = "idna" }, + { name = "sniffio", marker = "python_full_version >= '3.13'" }, + { name = "sortedcontainers", marker = "python_full_version >= '3.13'" }, + ] + + [[package]] + name = "sniffio" + version = "1.3.1" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, + ] + + [[package]] + name = "sortedcontainers" + version = "2.4.0" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, + ] + + [[package]] + name = "typing-extensions" + version = "4.10.0" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/16/3a/0d26ce356c7465a19c9ea8814b960f8a36c3b0d07c323176620b7b483e44/typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb", size = 77558, upload-time = "2024-02-25T22:12:49.693Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/de/dc04a3ea60b22624b51c703a84bbe0184abcd1d0b9bc8074b5d6b7ab90bb/typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475", size = 33926, upload-time = "2024-02-25T22:12:47.72Z" }, + ] + "# + ); + }); + + Ok(()) +} + +/// Referring to a dependency-group with group-requires-python that does not exist +#[test] +fn lock_group_requires_undefined_group() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "myproject" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["typing-extensions"] + + [dependency-groups] + bar = ["sortedcontainers"] + + [tool.uv.dependency-groups] + foo = { requires-python = ">=3.13" } + "#, + )?; + + uv_snapshot!(context.filters(), context.lock(), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Project `myproject` has malformed dependency groups + Caused by: Failed to find group `foo` specified in `[tool.uv.dependency-groups]` + "); + Ok(()) +} + +/// The legacy dev-dependencies cannot be referred to by group-requires-python +#[test] +fn lock_group_requires_dev_dep() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "myproject" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["typing-extensions"] + + [tool.uv] + dev-dependencies = ["sortedcontainers"] + + [tool.uv.dependency-groups] + dev = { requires-python = ">=3.13" } + "#, + )?; + + uv_snapshot!(context.filters(), context.lock(), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Project `myproject` has malformed dependency groups + Caused by: `[tool.uv.dependency-groups]` specifies the `dev` group, but only `tool.uv.dev-dependencies` was found. To reference the `dev` group, remove the `tool.uv.dev-dependencies` section and add any development dependencies to the `dev` entry in the `[dependency-groups]` table instead. + "); + Ok(()) +} + +#[test] +fn lock_group_includes_requires_python_contradiction() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["typing-extensions"] + + [dependency-groups] + foo = ["idna", {include-group = "bar"}] + bar = ["sortedcontainers", "sniffio"] + + [tool.uv.dependency-groups] + bar = { requires-python = ">=3.13" } + foo = { requires-python = "<3.13" } + "#, + )?; + + uv_snapshot!(context.filters(), context.lock(), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 5 packages in [TIME] + "); + + let lock = context.read("uv.lock"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r#" + version = 1 + revision = 2 + requires-python = ">=3.12" + resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version < '3.13'", + ] + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [[package]] + name = "idna" + version = "3.6" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426, upload-time = "2023-11-25T15:40:54.902Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567, upload-time = "2023-11-25T15:40:52.604Z" }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "typing-extensions" }, + ] + + [package.dev-dependencies] + bar = [ + { name = "sniffio", marker = "python_full_version >= '3.13'" }, + { name = "sortedcontainers", marker = "python_full_version >= '3.13'" }, + ] + foo = [ + { name = "idna", marker = "python_full_version < '3.13'" }, + ] + + [package.metadata] + requires-dist = [{ name = "typing-extensions" }] + + [package.metadata.requires-dev] + bar = [ + { name = "sniffio", marker = "python_full_version >= '3.13'" }, + { name = "sortedcontainers", marker = "python_full_version >= '3.13'" }, + ] + foo = [ + { name = "idna", marker = "python_full_version < '3.13'" }, + { name = "sniffio", marker = "python_version < '0'" }, + { name = "sortedcontainers", marker = "python_version < '0'" }, + ] + + [[package]] + name = "sniffio" + version = "1.3.1" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, + ] + + [[package]] + name = "sortedcontainers" + version = "2.4.0" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, + ] + + [[package]] + name = "typing-extensions" + version = "4.10.0" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/16/3a/0d26ce356c7465a19c9ea8814b960f8a36c3b0d07c323176620b7b483e44/typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb", size = 77558, upload-time = "2024-02-25T22:12:49.693Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/de/dc04a3ea60b22624b51c703a84bbe0184abcd1d0b9bc8074b5d6b7ab90bb/typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475", size = 33926, upload-time = "2024-02-25T22:12:47.72Z" }, + ] + "# + ); + }); + + Ok(()) +} + #[test] fn lock_group_include_cycle() -> Result<()> { let context = TestContext::new("3.12"); @@ -20680,15 +21140,15 @@ fn lock_group_include_cycle() -> Result<()> { "#, )?; - uv_snapshot!(context.filters(), context.lock(), @r###" + uv_snapshot!(context.filters(), context.lock(), @r" success: false - exit_code: 1 + exit_code: 2 ----- stdout ----- ----- stderr ----- - × Failed to build `project @ file://[TEMP_DIR]/` - ╰─▶ Detected a cycle in `dependency-groups`: `bar` -> `foobar` -> `foo` -> `bar` - "###); + error: Project `project` has malformed dependency groups + Caused by: Detected a cycle in `dependency-groups`: `bar` -> `foobar` -> `foo` -> `bar` + "); Ok(()) } @@ -20714,15 +21174,15 @@ fn lock_group_include_dev() -> Result<()> { "#, )?; - uv_snapshot!(context.filters(), context.lock(), @r###" + uv_snapshot!(context.filters(), context.lock(), @r#" success: false - exit_code: 1 + exit_code: 2 ----- stdout ----- ----- stderr ----- - × Failed to build `project @ file://[TEMP_DIR]/` - ╰─▶ Group `foo` includes the `dev` group (`include = "dev"`), but only `tool.uv.dev-dependencies` was found. To reference the `dev` group via an `include`, remove the `tool.uv.dev-dependencies` section and add any development dependencies to the `dev` entry in the `[dependency-groups]` table instead. - "###); + error: Project `project` has malformed dependency groups + Caused by: Group `foo` includes the `dev` group (`include = "dev"`), but only `tool.uv.dev-dependencies` was found. To reference the `dev` group via an `include`, remove the `tool.uv.dev-dependencies` section and add any development dependencies to the `dev` entry in the `[dependency-groups]` table instead. + "#); Ok(()) } @@ -20745,15 +21205,15 @@ fn lock_group_include_missing() -> Result<()> { "#, )?; - uv_snapshot!(context.filters(), context.lock(), @r###" + uv_snapshot!(context.filters(), context.lock(), @r" success: false - exit_code: 1 + exit_code: 2 ----- stdout ----- ----- stderr ----- - × Failed to build `project @ file://[TEMP_DIR]/` - ╰─▶ Failed to find group `bar` included by `foo` - "###); + error: Project `project` has malformed dependency groups + Caused by: Failed to find group `bar` included by `foo` + "); Ok(()) } @@ -20776,31 +21236,31 @@ fn lock_group_invalid_entry_package() -> Result<()> { "#, )?; - uv_snapshot!(context.filters(), context.lock(), @r###" + uv_snapshot!(context.filters(), context.lock(), @r#" success: false - exit_code: 1 + exit_code: 2 ----- stdout ----- ----- stderr ----- - × Failed to build `project @ file://[TEMP_DIR]/` - ├─▶ Failed to parse entry in group `foo`: `invalid!` - ╰─▶ no such comparison operator "!", must be one of ~= == != <= >= < > === - invalid! - ^ - "###); + error: Project `project` has malformed dependency groups + Caused by: Failed to parse entry in group `foo`: `invalid!` + Caused by: no such comparison operator "!", must be one of ~= == != <= >= < > === + invalid! + ^ + "#); - uv_snapshot!(context.filters(), context.sync().arg("--group").arg("foo"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--group").arg("foo"), @r#" success: false - exit_code: 1 + exit_code: 2 ----- stdout ----- ----- stderr ----- - × Failed to build `project @ file://[TEMP_DIR]/` - ├─▶ Failed to parse entry in group `foo`: `invalid!` - ╰─▶ no such comparison operator "!", must be one of ~= == != <= >= < > === - invalid! - ^ - "###); + error: Project `project` has malformed dependency groups + Caused by: Failed to parse entry in group `foo`: `invalid!` + Caused by: no such comparison operator "!", must be one of ~= == != <= >= < > === + invalid! + ^ + "#); Ok(()) } @@ -20897,12 +21357,12 @@ fn lock_group_invalid_entry_table() -> Result<()> { uv_snapshot!(context.filters(), context.lock(), @r#" success: false - exit_code: 1 + exit_code: 2 ----- stdout ----- ----- stderr ----- - × Failed to build `project @ file://[TEMP_DIR]/` - ╰─▶ Group `foo` contains an unknown dependency object specifier: {"bar": "unknown"} + error: Project `project` has malformed dependency groups + Caused by: Group `foo` contains an unknown dependency object specifier: {"bar": "unknown"} "#); Ok(()) diff --git a/crates/uv/tests/it/python_find.rs b/crates/uv/tests/it/python_find.rs index 11bf421a3..f438e9b4d 100644 --- a/crates/uv/tests/it/python_find.rs +++ b/crates/uv/tests/it/python_find.rs @@ -318,15 +318,15 @@ fn python_find_project() { "###); // Unless explicitly requested - uv_snapshot!(context.filters(), context.python_find().arg("3.10"), @r###" + uv_snapshot!(context.filters(), context.python_find().arg("3.10"), @r" success: true exit_code: 0 ----- stdout ----- [PYTHON-3.10] ----- stderr ----- - warning: The requested interpreter resolved to Python 3.10.[X], which is incompatible with the project's Python requirement: `>=3.11` - "###); + warning: The requested interpreter resolved to Python 3.10.[X], which is incompatible with the project's Python requirement: `>=3.11` (from `project.requires-python`) + "); // Or `--no-project` is used uv_snapshot!(context.filters(), context.python_find().arg("--no-project"), @r###" @@ -367,15 +367,16 @@ fn python_find_project() { "###); // We should warn on subsequent uses, but respect the pinned version? - uv_snapshot!(context.filters(), context.python_find(), @r###" + uv_snapshot!(context.filters(), context.python_find(), @r" success: true exit_code: 0 ----- stdout ----- [PYTHON-3.10] ----- stderr ----- - warning: The Python request from `.python-version` resolved to Python 3.10.[X], which is incompatible with the project's Python requirement: `>=3.11`. Use `uv python pin` to update the `.python-version` file to a compatible version. - "###); + warning: The Python request from `.python-version` resolved to Python 3.10.[X], which is incompatible with the project's Python requirement: `>=3.11` (from `project.requires-python`) + Use `uv python pin` to update the `.python-version` file to a compatible version + "); // Unless the pin file is outside the project, in which case we should just ignore it let child_dir = context.temp_dir.child("child"); diff --git a/crates/uv/tests/it/run.rs b/crates/uv/tests/it/run.rs index 121915ef0..65d13c527 100644 --- a/crates/uv/tests/it/run.rs +++ b/crates/uv/tests/it/run.rs @@ -133,7 +133,7 @@ fn run_with_python_version() -> Result<()> { ----- stderr ----- Using CPython 3.9.[X] interpreter at: [PYTHON-3.9] - error: The requested interpreter resolved to Python 3.9.[X], which is incompatible with the project's Python requirement: `>=3.11, <4` + error: The requested interpreter resolved to Python 3.9.[X], which is incompatible with the project's Python requirement: `>=3.11, <4` (from `project.requires-python`) "); Ok(()) @@ -3136,25 +3136,27 @@ fn run_isolated_incompatible_python() -> Result<()> { })?; // We should reject Python 3.9... - uv_snapshot!(context.filters(), context.run().arg("main.py"), @r###" + uv_snapshot!(context.filters(), context.run().arg("main.py"), @r" success: false exit_code: 2 ----- stdout ----- ----- stderr ----- Using CPython 3.9.[X] interpreter at: [PYTHON-3.9] - error: The Python request from `.python-version` resolved to Python 3.9.[X], which is incompatible with the project's Python requirement: `>=3.12`. Use `uv python pin` to update the `.python-version` file to a compatible version. - "###); + error: The Python request from `.python-version` resolved to Python 3.9.[X], which is incompatible with the project's Python requirement: `>=3.12` (from `project.requires-python`) + Use `uv python pin` to update the `.python-version` file to a compatible version + "); // ...even if `--isolated` is provided. - uv_snapshot!(context.filters(), context.run().arg("--isolated").arg("main.py"), @r###" + uv_snapshot!(context.filters(), context.run().arg("--isolated").arg("main.py"), @r" success: false exit_code: 2 ----- stdout ----- ----- stderr ----- - error: The Python request from `.python-version` resolved to Python 3.9.[X], which is incompatible with the project's Python requirement: `>=3.12`. Use `uv python pin` to update the `.python-version` file to a compatible version. - "###); + error: The Python request from `.python-version` resolved to Python 3.9.[X], which is incompatible with the project's Python requirement: `>=3.12` (from `project.requires-python`) + Use `uv python pin` to update the `.python-version` file to a compatible version + "); Ok(()) } @@ -4598,6 +4600,249 @@ fn run_default_groups() -> Result<()> { Ok(()) } + +#[test] +fn run_groups_requires_python() -> Result<()> { + let context = + TestContext::new_with_versions(&["3.11", "3.12", "3.13"]).with_filtered_python_sources(); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.11" + dependencies = ["typing-extensions"] + + [dependency-groups] + foo = ["anyio"] + bar = ["iniconfig"] + dev = ["sniffio"] + + [tool.uv.dependency-groups] + foo = {requires-python=">=3.14"} + bar = {requires-python=">=3.13"} + dev = {requires-python=">=3.12"} + "#, + )?; + + context.lock().assert().success(); + + // With --no-default-groups only the main requires-python should be consulted + uv_snapshot!(context.filters(), context.run() + .arg("--no-default-groups") + .arg("python").arg("-c").arg("import typing_extensions"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.11.[X] interpreter at: [PYTHON-3.11] + Creating virtual environment at: .venv + Resolved 6 packages in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + typing-extensions==4.10.0 + "); + + // The main requires-python and the default group's requires-python should be consulted + // (This should trigger a version bump) + uv_snapshot!(context.filters(), context.run() + .arg("python").arg("-c").arg("import typing_extensions"), @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 6 packages in [TIME] + Prepared 1 package in [TIME] + Installed 2 packages in [TIME] + + sniffio==1.3.1 + + typing-extensions==4.10.0 + "); + + // The main requires-python and "dev" and "bar" requires-python should be consulted + // (This should trigger a version bump) + uv_snapshot!(context.filters(), context.run() + .arg("--group").arg("bar") + .arg("python").arg("-c").arg("import typing_extensions"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.13.[X] interpreter at: [PYTHON-3.13] + Removed virtual environment at: .venv + Creating virtual environment at: .venv + Resolved 6 packages in [TIME] + Prepared 1 package in [TIME] + Installed 3 packages in [TIME] + + iniconfig==2.0.0 + + sniffio==1.3.1 + + typing-extensions==4.10.0 + "); + + // Going back to just "dev" we shouldn't churn the venv needlessly + uv_snapshot!(context.filters(), context.run() + .arg("python").arg("-c").arg("import typing_extensions"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 6 packages in [TIME] + Audited 2 packages in [TIME] + "); + + // Explicitly requesting an in-range python can downgrade + uv_snapshot!(context.filters(), context.run() + .arg("-p").arg("3.12") + .arg("python").arg("-c").arg("import typing_extensions"), @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 6 packages in [TIME] + Installed 2 packages in [TIME] + + sniffio==1.3.1 + + typing-extensions==4.10.0 + "); + + // Explicitly requesting an out-of-range python fails + uv_snapshot!(context.filters(), context.run() + .arg("-p").arg("3.11") + .arg("python").arg("-c").arg("import typing_extensions"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.11.[X] interpreter at: [PYTHON-3.11] + error: The requested interpreter resolved to Python 3.11.[X], which is incompatible with the project's Python requirement: `>=3.12` (from `tool.uv.dependency-groups.dev.requires-python`). + "); + + // Enabling foo we can't find an interpreter + uv_snapshot!(context.filters(), context.run() + .arg("--group").arg("foo") + .arg("python").arg("-c").arg("import typing_extensions"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: No interpreter found for Python >=3.14 in [PYTHON SOURCES] + "); + + Ok(()) +} + +#[test] +fn run_groups_include_requires_python() -> Result<()> { + let context = TestContext::new_with_versions(&["3.11", "3.12", "3.13"]); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.11" + dependencies = ["typing-extensions"] + + [dependency-groups] + foo = ["anyio"] + bar = ["iniconfig"] + baz = ["iniconfig"] + dev = ["sniffio", {include-group = "foo"}, {include-group = "baz"}] + + + [tool.uv.dependency-groups] + foo = {requires-python="<3.13"} + bar = {requires-python=">=3.13"} + baz = {requires-python=">=3.12"} + "#, + )?; + + context.lock().assert().success(); + + // With --no-default-groups only the main requires-python should be consulted + uv_snapshot!(context.filters(), context.run() + .arg("--no-default-groups") + .arg("python").arg("-c").arg("import typing_extensions"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.11.[X] interpreter at: [PYTHON-3.11] + Creating virtual environment at: .venv + Resolved 6 packages in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + typing-extensions==4.10.0 + "); + + // The main requires-python and the default group's requires-python should be consulted + // (This should trigger a version bump) + uv_snapshot!(context.filters(), context.run() + .arg("python").arg("-c").arg("import typing_extensions"), @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 6 packages in [TIME] + Prepared 4 packages in [TIME] + Installed 5 packages in [TIME] + + anyio==4.3.0 + + idna==3.6 + + iniconfig==2.0.0 + + sniffio==1.3.1 + + typing-extensions==4.10.0 + "); + + // The main requires-python and "dev" and "bar" requires-python should be consulted + // (This should trigger a conflict) + uv_snapshot!(context.filters(), context.run() + .arg("--group").arg("bar") + .arg("python").arg("-c").arg("import typing_extensions"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Found conflicting Python requirements: + - project: >=3.11 + - project:bar: >=3.13 + - project:dev: >=3.12, <3.13 + "); + + // Explicitly requesting an out-of-range python fails + uv_snapshot!(context.filters(), context.run() + .arg("-p").arg("3.13") + .arg("python").arg("-c").arg("import typing_extensions"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.13.[X] interpreter at: [PYTHON-3.13] + error: The requested interpreter resolved to Python 3.13.[X], which is incompatible with the project's Python requirement: `==3.12.*` (from `tool.uv.dependency-groups.dev.requires-python`). + "); + Ok(()) +} + /// Test that a signal n makes the process exit with code 128+n. #[cfg(unix)] #[test] diff --git a/crates/uv/tests/it/show_settings.rs b/crates/uv/tests/it/show_settings.rs index 87453090c..7635bd523 100644 --- a/crates/uv/tests/it/show_settings.rs +++ b/crates/uv/tests/it/show_settings.rs @@ -3987,7 +3987,7 @@ fn resolve_config_file() -> anyhow::Result<()> { | 1 | [project] | ^^^^^^^ - unknown field `project`, expected one of `required-version`, `native-tls`, `offline`, `no-cache`, `cache-dir`, `preview`, `python-preference`, `python-downloads`, `concurrent-downloads`, `concurrent-builds`, `concurrent-installs`, `index`, `index-url`, `extra-index-url`, `no-index`, `find-links`, `index-strategy`, `keyring-provider`, `allow-insecure-host`, `resolution`, `prerelease`, `fork-strategy`, `dependency-metadata`, `config-settings`, `no-build-isolation`, `no-build-isolation-package`, `exclude-newer`, `link-mode`, `compile-bytecode`, `no-sources`, `upgrade`, `upgrade-package`, `reinstall`, `reinstall-package`, `no-build`, `no-build-package`, `no-binary`, `no-binary-package`, `python-install-mirror`, `pypy-install-mirror`, `python-downloads-json-url`, `publish-url`, `trusted-publishing`, `check-url`, `add-bounds`, `pip`, `cache-keys`, `override-dependencies`, `constraint-dependencies`, `build-constraint-dependencies`, `environments`, `required-environments`, `conflicts`, `workspace`, `sources`, `managed`, `package`, `default-groups`, `dev-dependencies`, `build-backend` + unknown field `project`, expected one of `required-version`, `native-tls`, `offline`, `no-cache`, `cache-dir`, `preview`, `python-preference`, `python-downloads`, `concurrent-downloads`, `concurrent-builds`, `concurrent-installs`, `index`, `index-url`, `extra-index-url`, `no-index`, `find-links`, `index-strategy`, `keyring-provider`, `allow-insecure-host`, `resolution`, `prerelease`, `fork-strategy`, `dependency-metadata`, `config-settings`, `no-build-isolation`, `no-build-isolation-package`, `exclude-newer`, `link-mode`, `compile-bytecode`, `no-sources`, `upgrade`, `upgrade-package`, `reinstall`, `reinstall-package`, `no-build`, `no-build-package`, `no-binary`, `no-binary-package`, `python-install-mirror`, `pypy-install-mirror`, `python-downloads-json-url`, `publish-url`, `trusted-publishing`, `check-url`, `add-bounds`, `pip`, `cache-keys`, `override-dependencies`, `constraint-dependencies`, `build-constraint-dependencies`, `environments`, `required-environments`, `conflicts`, `workspace`, `sources`, `managed`, `package`, `default-groups`, `dependency-groups`, `dev-dependencies`, `build-backend` " ); diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index 3f3cd072c..70d8a9118 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -346,7 +346,296 @@ fn mixed_requires_python() -> Result<()> { ----- stderr ----- Using CPython 3.9.[X] interpreter at: [PYTHON-3.9] - error: The requested interpreter resolved to Python 3.9.[X], which is incompatible with the project's Python requirement: `>=3.12`. However, a workspace member (`bird-feeder`) supports Python >=3.9. To install the workspace member on its own, navigate to `packages/bird-feeder`, then run `uv venv --python 3.9.[X]` followed by `uv pip install -e .`. + error: The requested interpreter resolved to Python 3.9.[X], which is incompatible with the project's Python requirement: `>=3.12` (from workspace member `albatross`'s `project.requires-python`). + "); + + Ok(()) +} + +/// Ensure that group requires-python solves an actual problem +#[test] +#[cfg(not(windows))] +fn group_requires_python_useful_defaults() -> Result<()> { + let context = TestContext::new_with_versions(&["3.8", "3.9"]); + + // Require 3.8 for our project, but have a dev-dependency on a version of sphinx that needs 3.9 + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "pharaohs-tomp" + version = "0.1.0" + requires-python = ">=3.8" + dependencies = ["anyio"] + + [dependency-groups] + dev = ["sphinx>=7.2.6"] + "#, + )?; + + let src = context.temp_dir.child("src").child("albatross"); + src.create_dir_all()?; + + let init = src.child("__init__.py"); + init.touch()?; + + // Running `uv sync --no-dev` should ideally succeed, locking for Python 3.8. + // ...but once we pick the 3.8 interpreter the lock freaks out because it sees + // that the dependency-group containing sphinx will never successfully install, + // even though it's not enabled! + uv_snapshot!(context.filters(), context.sync() + .arg("--no-dev"), @r" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.8.[X] interpreter at: [PYTHON-3.8] + Creating virtual environment at: .venv + × No solution found when resolving dependencies for split (python_full_version == '3.8.*'): + ╰─▶ Because the requested Python version (>=3.8) does not satisfy Python>=3.9 and sphinx==7.2.6 depends on Python>=3.9, we can conclude that sphinx==7.2.6 cannot be used. + And because only sphinx<=7.2.6 is available, we can conclude that sphinx>=7.2.6 cannot be used. + And because pharaohs-tomp:dev depends on sphinx>=7.2.6 and your project requires pharaohs-tomp:dev, we can conclude that your project's requirements are unsatisfiable. + + hint: The `requires-python` value (>=3.8) includes Python versions that are not supported by your dependencies (e.g., sphinx==7.2.6 only supports >=3.9). Consider using a more restrictive `requires-python` value (like >=3.9). + "); + + // Running `uv sync` should always fail, as now sphinx is involved + uv_snapshot!(context.filters(), context.sync(), @r" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies for split (python_full_version == '3.8.*'): + ╰─▶ Because the requested Python version (>=3.8) does not satisfy Python>=3.9 and sphinx==7.2.6 depends on Python>=3.9, we can conclude that sphinx==7.2.6 cannot be used. + And because only sphinx<=7.2.6 is available, we can conclude that sphinx>=7.2.6 cannot be used. + And because pharaohs-tomp:dev depends on sphinx>=7.2.6 and your project requires pharaohs-tomp:dev, we can conclude that your project's requirements are unsatisfiable. + + hint: The `requires-python` value (>=3.8) includes Python versions that are not supported by your dependencies (e.g., sphinx==7.2.6 only supports >=3.9). Consider using a more restrictive `requires-python` value (like >=3.9). + "); + + // Adding group requires python should fix it + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "pharaohs-tomp" + version = "0.1.0" + requires-python = ">=3.8" + dependencies = ["anyio"] + + [dependency-groups] + dev = ["sphinx>=7.2.6"] + + [tool.uv.dependency-groups] + dev = {requires-python = ">=3.9"} + "#, + )?; + + // Running `uv sync --no-dev` should succeed, still using the Python 3.8. + uv_snapshot!(context.filters(), context.sync() + .arg("--no-dev"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 29 packages in [TIME] + Prepared 5 packages in [TIME] + Installed 5 packages in [TIME] + + anyio==4.3.0 + + exceptiongroup==1.2.0 + + idna==3.6 + + sniffio==1.3.1 + + typing-extensions==4.10.0 + "); + + // Running `uv sync` should succeed, bumping to Python 3.9 as sphinx is now involved. + uv_snapshot!(context.filters(), context.sync(), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.9.[X] interpreter at: [PYTHON-3.9] + Removed virtual environment at: .venv + Creating virtual environment at: .venv + Resolved 29 packages in [TIME] + Prepared 22 packages in [TIME] + Installed 27 packages in [TIME] + + alabaster==0.7.16 + + anyio==4.3.0 + + babel==2.14.0 + + certifi==2024.2.2 + + charset-normalizer==3.3.2 + + docutils==0.20.1 + + exceptiongroup==1.2.0 + + idna==3.6 + + imagesize==1.4.1 + + importlib-metadata==7.1.0 + + jinja2==3.1.3 + + markupsafe==2.1.5 + + packaging==24.0 + + pygments==2.17.2 + + requests==2.31.0 + + sniffio==1.3.1 + + snowballstemmer==2.2.0 + + sphinx==7.2.6 + + sphinxcontrib-applehelp==1.0.8 + + sphinxcontrib-devhelp==1.0.6 + + sphinxcontrib-htmlhelp==2.0.5 + + sphinxcontrib-jsmath==1.0.1 + + sphinxcontrib-qthelp==1.0.7 + + sphinxcontrib-serializinghtml==1.1.10 + + typing-extensions==4.10.0 + + urllib3==2.2.1 + + zipp==3.18.1 + "); + + Ok(()) +} + +/// Ensure that group requires-python solves an actual problem +#[test] +#[cfg(not(windows))] +fn group_requires_python_useful_non_defaults() -> Result<()> { + let context = TestContext::new_with_versions(&["3.8", "3.9"]); + + // Require 3.8 for our project, but have a dev-dependency on a version of sphinx that needs 3.9 + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "pharaohs-tomp" + version = "0.1.0" + requires-python = ">=3.8" + dependencies = ["anyio"] + + [dependency-groups] + mygroup = ["sphinx>=7.2.6"] + "#, + )?; + + let src = context.temp_dir.child("src").child("albatross"); + src.create_dir_all()?; + + let init = src.child("__init__.py"); + init.touch()?; + + // Running `uv sync` should ideally succeed, locking for Python 3.8. + // ...but once we pick the 3.8 interpreter the lock freaks out because it sees + // that the dependency-group containing sphinx will never successfully install, + // even though it's not enabled, or even a default! + uv_snapshot!(context.filters(), context.sync(), @r" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.8.[X] interpreter at: [PYTHON-3.8] + Creating virtual environment at: .venv + × No solution found when resolving dependencies for split (python_full_version == '3.8.*'): + ╰─▶ Because the requested Python version (>=3.8) does not satisfy Python>=3.9 and sphinx==7.2.6 depends on Python>=3.9, we can conclude that sphinx==7.2.6 cannot be used. + And because only sphinx<=7.2.6 is available, we can conclude that sphinx>=7.2.6 cannot be used. + And because pharaohs-tomp:mygroup depends on sphinx>=7.2.6 and your project requires pharaohs-tomp:mygroup, we can conclude that your project's requirements are unsatisfiable. + + hint: The `requires-python` value (>=3.8) includes Python versions that are not supported by your dependencies (e.g., sphinx==7.2.6 only supports >=3.9). Consider using a more restrictive `requires-python` value (like >=3.9). + "); + + // Running `uv sync --group mygroup` should definitely fail, as now sphinx is involved + uv_snapshot!(context.filters(), context.sync() + .arg("--group").arg("mygroup"), @r" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies for split (python_full_version == '3.8.*'): + ╰─▶ Because the requested Python version (>=3.8) does not satisfy Python>=3.9 and sphinx==7.2.6 depends on Python>=3.9, we can conclude that sphinx==7.2.6 cannot be used. + And because only sphinx<=7.2.6 is available, we can conclude that sphinx>=7.2.6 cannot be used. + And because pharaohs-tomp:mygroup depends on sphinx>=7.2.6 and your project requires pharaohs-tomp:mygroup, we can conclude that your project's requirements are unsatisfiable. + + hint: The `requires-python` value (>=3.8) includes Python versions that are not supported by your dependencies (e.g., sphinx==7.2.6 only supports >=3.9). Consider using a more restrictive `requires-python` value (like >=3.9). + "); + + // Adding group requires python should fix it + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "pharaohs-tomp" + version = "0.1.0" + requires-python = ">=3.8" + dependencies = ["anyio"] + + [dependency-groups] + mygroup = ["sphinx>=7.2.6"] + + [tool.uv.dependency-groups] + mygroup = {requires-python = ">=3.9"} + "#, + )?; + + // Running `uv sync` should succeed, locking for the previous picked Python 3.8. + uv_snapshot!(context.filters(), context.sync(), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 29 packages in [TIME] + Prepared 5 packages in [TIME] + Installed 5 packages in [TIME] + + anyio==4.3.0 + + exceptiongroup==1.2.0 + + idna==3.6 + + sniffio==1.3.1 + + typing-extensions==4.10.0 + "); + + // Running `uv sync --group mygroup` should pass, bumping the interpreter to 3.9, + // as the group requires-python saves us + uv_snapshot!(context.filters(), context.sync() + .arg("--group").arg("mygroup"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.9.[X] interpreter at: [PYTHON-3.9] + Removed virtual environment at: .venv + Creating virtual environment at: .venv + Resolved 29 packages in [TIME] + Prepared 22 packages in [TIME] + Installed 27 packages in [TIME] + + alabaster==0.7.16 + + anyio==4.3.0 + + babel==2.14.0 + + certifi==2024.2.2 + + charset-normalizer==3.3.2 + + docutils==0.20.1 + + exceptiongroup==1.2.0 + + idna==3.6 + + imagesize==1.4.1 + + importlib-metadata==7.1.0 + + jinja2==3.1.3 + + markupsafe==2.1.5 + + packaging==24.0 + + pygments==2.17.2 + + requests==2.31.0 + + sniffio==1.3.1 + + snowballstemmer==2.2.0 + + sphinx==7.2.6 + + sphinxcontrib-applehelp==1.0.8 + + sphinxcontrib-devhelp==1.0.6 + + sphinxcontrib-htmlhelp==2.0.5 + + sphinxcontrib-jsmath==1.0.1 + + sphinxcontrib-qthelp==1.0.7 + + sphinxcontrib-serializinghtml==1.1.10 + + typing-extensions==4.10.0 + + urllib3==2.2.1 + + zipp==3.18.1 "); Ok(()) @@ -4080,17 +4369,17 @@ fn sync_custom_environment_path() -> Result<()> { // But if it's just an incompatible virtual environment... fs_err::remove_dir_all(context.temp_dir.join("foo"))?; - uv_snapshot!(context.filters(), context.venv().arg("foo").arg("--python").arg("3.11"), @r###" + uv_snapshot!(context.filters(), context.venv().arg("foo").arg("--python").arg("3.11"), @r" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Using CPython 3.11.[X] interpreter at: [PYTHON-3.11] - warning: The requested interpreter resolved to Python 3.11.[X], which is incompatible with the project's Python requirement: `>=3.12` + warning: The requested interpreter resolved to Python 3.11.[X], which is incompatible with the project's Python requirement: `>=3.12` (from `project.requires-python`) Creating virtual environment at: foo Activate with: source foo/[BIN]/activate - "###); + "); // Even with some extraneous content... fs_err::write(context.temp_dir.join("foo").join("file"), b"")?; @@ -5817,17 +6106,17 @@ fn sync_invalid_environment() -> Result<()> { // But if it's just an incompatible virtual environment... fs_err::remove_dir_all(context.temp_dir.join(".venv"))?; - uv_snapshot!(context.filters(), context.venv().arg("--python").arg("3.11"), @r###" + uv_snapshot!(context.filters(), context.venv().arg("--python").arg("3.11"), @r" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Using CPython 3.11.[X] interpreter at: [PYTHON-3.11] - warning: The requested interpreter resolved to Python 3.11.[X], which is incompatible with the project's Python requirement: `>=3.12` + warning: The requested interpreter resolved to Python 3.11.[X], which is incompatible with the project's Python requirement: `>=3.12` (from `project.requires-python`) Creating virtual environment at: .venv Activate with: source .venv/[BIN]/activate - "###); + "); // Even with some extraneous content... fs_err::write(context.temp_dir.join(".venv").join("file"), b"")?; @@ -5884,17 +6173,17 @@ fn sync_invalid_environment() -> Result<()> { // But if it's not a virtual environment... fs_err::remove_dir_all(context.temp_dir.join(".venv"))?; - uv_snapshot!(context.filters(), context.venv().arg("--python").arg("3.11"), @r###" + uv_snapshot!(context.filters(), context.venv().arg("--python").arg("3.11"), @r" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Using CPython 3.11.[X] interpreter at: [PYTHON-3.11] - warning: The requested interpreter resolved to Python 3.11.[X], which is incompatible with the project's Python requirement: `>=3.12` + warning: The requested interpreter resolved to Python 3.11.[X], which is incompatible with the project's Python requirement: `>=3.12` (from `project.requires-python`) Creating virtual environment at: .venv Activate with: source .venv/[BIN]/activate - "###); + "); // Which we detect by the presence of a `pyvenv.cfg` file fs_err::remove_file(context.temp_dir.join(".venv").join("pyvenv.cfg"))?; @@ -6004,15 +6293,15 @@ fn sync_python_version() -> Result<()> { "###); // Unless explicitly requested... - uv_snapshot!(context.filters(), context.sync().arg("--python").arg("3.10"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--python").arg("3.10"), @r" success: false exit_code: 2 ----- stdout ----- ----- stderr ----- Using CPython 3.10.[X] interpreter at: [PYTHON-3.10] - error: The requested interpreter resolved to Python 3.10.[X], which is incompatible with the project's Python requirement: `>=3.11` - "###); + error: The requested interpreter resolved to Python 3.10.[X], which is incompatible with the project's Python requirement: `>=3.11` (from `project.requires-python`) + "); // But a pin should take precedence uv_snapshot!(context.filters(), context.python_pin().arg("3.12"), @r###" @@ -6051,15 +6340,16 @@ fn sync_python_version() -> Result<()> { "###); // We should warn on subsequent uses, but respect the pinned version? - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: false exit_code: 2 ----- stdout ----- ----- stderr ----- Using CPython 3.10.[X] interpreter at: [PYTHON-3.10] - error: The Python request from `.python-version` resolved to Python 3.10.[X], which is incompatible with the project's Python requirement: `>=3.11`. Use `uv python pin` to update the `.python-version` file to a compatible version. - "###); + error: The Python request from `.python-version` resolved to Python 3.10.[X], which is incompatible with the project's Python requirement: `>=3.11` (from `project.requires-python`) + Use `uv python pin` to update the `.python-version` file to a compatible version + "); // Unless the pin file is outside the project, in which case we should just ignore it entirely let child_dir = context.temp_dir.child("child"); @@ -8935,52 +9225,52 @@ fn transitive_group_conflicts_cycle() -> Result<()> { uv_snapshot!(context.filters(), context.sync(), @r" success: false - exit_code: 1 + exit_code: 2 ----- stdout ----- ----- stderr ----- - × Failed to build `example @ file://[TEMP_DIR]/` - ╰─▶ Detected a cycle in `dependency-groups`: `dev` -> `test` -> `dev` + error: Project `example` has malformed dependency groups + Caused by: Detected a cycle in `dependency-groups`: `dev` -> `test` -> `dev` "); uv_snapshot!(context.filters(), context.sync().arg("--group").arg("dev"), @r" success: false - exit_code: 1 + exit_code: 2 ----- stdout ----- ----- stderr ----- - × Failed to build `example @ file://[TEMP_DIR]/` - ╰─▶ Detected a cycle in `dependency-groups`: `dev` -> `test` -> `dev` + error: Project `example` has malformed dependency groups + Caused by: Detected a cycle in `dependency-groups`: `dev` -> `test` -> `dev` "); uv_snapshot!(context.filters(), context.sync().arg("--group").arg("dev").arg("--group").arg("test"), @r" success: false - exit_code: 1 + exit_code: 2 ----- stdout ----- ----- stderr ----- - × Failed to build `example @ file://[TEMP_DIR]/` - ╰─▶ Detected a cycle in `dependency-groups`: `dev` -> `test` -> `dev` + error: Project `example` has malformed dependency groups + Caused by: Detected a cycle in `dependency-groups`: `dev` -> `test` -> `dev` "); uv_snapshot!(context.filters(), context.sync().arg("--group").arg("test").arg("--group").arg("magic"), @r" success: false - exit_code: 1 + exit_code: 2 ----- stdout ----- ----- stderr ----- - × Failed to build `example @ file://[TEMP_DIR]/` - ╰─▶ Detected a cycle in `dependency-groups`: `dev` -> `test` -> `dev` + error: Project `example` has malformed dependency groups + Caused by: Detected a cycle in `dependency-groups`: `dev` -> `test` -> `dev` "); uv_snapshot!(context.filters(), context.sync().arg("--group").arg("dev").arg("--group").arg("magic"), @r" success: false - exit_code: 1 + exit_code: 2 ----- stdout ----- ----- stderr ----- - × Failed to build `example @ file://[TEMP_DIR]/` - ╰─▶ Detected a cycle in `dependency-groups`: `dev` -> `test` -> `dev` + error: Project `example` has malformed dependency groups + Caused by: Detected a cycle in `dependency-groups`: `dev` -> `test` -> `dev` "); Ok(()) diff --git a/crates/uv/tests/it/venv.rs b/crates/uv/tests/it/venv.rs index 1d9eb5721..bc35f9490 100644 --- a/crates/uv/tests/it/venv.rs +++ b/crates/uv/tests/it/venv.rs @@ -475,17 +475,195 @@ fn create_venv_respects_pyproject_requires_python() -> Result<()> { context.venv.assert(predicates::path::is_dir()); // We warn if we receive an incompatible version - uv_snapshot!(context.filters(), context.venv().arg("--python").arg("3.11"), @r###" + uv_snapshot!(context.filters(), context.venv().arg("--python").arg("3.11"), @r" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Using CPython 3.11.[X] interpreter at: [PYTHON-3.11] - warning: The requested interpreter resolved to Python 3.11.[X], which is incompatible with the project's Python requirement: `>=3.12` + warning: The requested interpreter resolved to Python 3.11.[X], which is incompatible with the project's Python requirement: `>=3.12` (from `project.requires-python`) Creating virtual environment at: .venv Activate with: source .venv/[BIN]/activate - "### + " + ); + + Ok(()) +} + +#[test] +fn create_venv_respects_group_requires_python() -> Result<()> { + let context = TestContext::new_with_versions(&["3.9", "3.10", "3.11", "3.12"]); + + // Without a Python requirement, we use the first on the PATH + uv_snapshot!(context.filters(), context.venv(), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.9.[X] interpreter at: [PYTHON-3.9] + Creating virtual environment at: .venv + Activate with: source .venv/[BIN]/activate + " + ); + + // With `requires-python = ">=3.10"` on the default group, we pick 3.10 + // However non-default groups should not be consulted! + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! { r#" + [project] + name = "foo" + version = "1.0.0" + dependencies = [] + + [dependency-groups] + dev = ["sortedcontainers"] + other = ["sniffio"] + + [tool.uv.dependency-groups] + dev = {requires-python = ">=3.10"} + other = {requires-python = ">=3.12"} + "# + })?; + + uv_snapshot!(context.filters(), context.venv(), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.10.[X] interpreter at: [PYTHON-3.10] + Creating virtual environment at: .venv + Activate with: source .venv/[BIN]/activate + " + ); + + // When the top-level requires-python and default group requires-python + // both apply, their intersection is used. However non-default groups + // should not be consulted! (here the top-level wins) + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! { r#" + [project] + name = "foo" + version = "1.0.0" + requires-python = ">=3.11" + dependencies = [] + + [dependency-groups] + dev = ["sortedcontainers"] + other = ["sniffio"] + + [tool.uv.dependency-groups] + dev = {requires-python = ">=3.10"} + other = {requires-python = ">=3.12"} + "# + })?; + + uv_snapshot!(context.filters(), context.venv(), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.11.[X] interpreter at: [PYTHON-3.11] + Creating virtual environment at: .venv + Activate with: source .venv/[BIN]/activate + " + ); + + // When the top-level requires-python and default group requires-python + // both apply, their intersection is used. However non-default groups + // should not be consulted! (here the group wins) + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! { r#" + [project] + name = "foo" + version = "1.0.0" + requires-python = ">=3.10" + dependencies = [] + + [dependency-groups] + dev = ["sortedcontainers"] + other = ["sniffio"] + + [tool.uv.dependency-groups] + dev = {requires-python = ">=3.11"} + other = {requires-python = ">=3.12"} + "# + })?; + + uv_snapshot!(context.filters(), context.venv(), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.11.[X] interpreter at: [PYTHON-3.11] + Creating virtual environment at: .venv + Activate with: source .venv/[BIN]/activate + " + ); + + // We warn if we receive an incompatible version + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! { r#" + [project] + name = "foo" + version = "1.0.0" + dependencies = [] + + [dependency-groups] + dev = ["sortedcontainers"] + + [tool.uv.dependency-groups] + dev = {requires-python = ">=3.12"} + "# + })?; + + uv_snapshot!(context.filters(), context.venv().arg("--python").arg("3.11"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.11.[X] interpreter at: [PYTHON-3.11] + warning: The requested interpreter resolved to Python 3.11.[X], which is incompatible with the project's Python requirement: `>=3.12` (from `tool.uv.dependency-groups.dev.requires-python`). + Creating virtual environment at: .venv + Activate with: source .venv/[BIN]/activate + " + ); + + // We error if there's no compatible version + // non-default groups are not consulted here! + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! { r#" + [project] + name = "foo" + version = "1.0.0" + requires-python = "<3.12" + dependencies = [] + + [dependency-groups] + dev = ["sortedcontainers"] + other = ["sniffio"] + + [tool.uv.dependency-groups] + dev = {requires-python = ">=3.12"} + other = {requires-python = ">=3.11"} + "# + })?; + + uv_snapshot!(context.filters(), context.venv().arg("--python").arg("3.11"), @r" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × Found conflicting Python requirements: + │ - foo: <3.12 + │ - foo:dev: >=3.12 + " ); Ok(()) diff --git a/docs/reference/settings.md b/docs/reference/settings.md index c203bcc71..f681690f4 100644 --- a/docs/reference/settings.md +++ b/docs/reference/settings.md @@ -127,6 +127,31 @@ default-groups = ["docs"] --- +### [`dependency-groups`](#dependency-groups) {: #dependency-groups } + +Additional settings for `dependency-groups`. + +Currently this can only be used to add `requires-python` constraints +to dependency groups (typically to inform uv that your dev tooling +has a higher python requirement than your actual project). + +This cannot be used to define dependency groups, use the top-level +`[dependency-groups]` table for that. + +**Default value**: `[]` + +**Type**: `dict` + +**Example usage**: + +```toml title="pyproject.toml" + +[tool.uv.dependency-groups] +my-group = {requires-python = ">=3.12"} +``` + +--- + ### [`dev-dependencies`](#dev-dependencies) {: #dev-dependencies } The project's development dependencies. diff --git a/uv.schema.json b/uv.schema.json index 97fe9e28e..33c1ff1f5 100644 --- a/uv.schema.json +++ b/uv.schema.json @@ -151,6 +151,17 @@ } ] }, + "dependency-groups": { + "description": "Additional settings for `dependency-groups`.\n\nCurrently this can only be used to add `requires-python` constraints to dependency groups (typically to inform uv that your dev tooling has a higher python requirement than your actual project).\n\nThis cannot be used to define dependency groups, use the top-level `[dependency-groups]` table for that.", + "anyOf": [ + { + "$ref": "#/definitions/ToolUvDependencyGroups" + }, + { + "type": "null" + } + ] + }, "dependency-metadata": { "description": "Pre-defined static metadata for dependencies of the project (direct or transitive). When provided, enables the resolver to use the specified metadata instead of querying the registry or building the relevant package from source.\n\nMetadata should be provided in adherence with the [Metadata 2.3](https://packaging.python.org/en/latest/specifications/core-metadata/) standard, though only the following fields are respected:\n\n- `name`: The name of the package. - (Optional) `version`: The version of the package. If omitted, the metadata will be applied to all versions of the package. - (Optional) `requires-dist`: The dependencies of the package (e.g., `werkzeug>=0.14`). - (Optional) `requires-python`: The Python version required by the package (e.g., `>=3.10`). - (Optional) `provides-extras`: The extras provided by the package.", "type": [ @@ -824,6 +835,18 @@ } ] }, + "DependencyGroupSettings": { + "type": "object", + "properties": { + "requires-python": { + "description": "Version of python to require when installing this group", + "type": [ + "string", + "null" + ] + } + } + }, "ExcludeNewer": { "description": "Exclude distributions uploaded after the given timestamp.\n\nAccepts both RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`) and local dates in the same format (e.g., `2006-12-02`).", "type": "string", @@ -2344,6 +2367,12 @@ } ] }, + "ToolUvDependencyGroups": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/DependencyGroupSettings" + } + }, "ToolUvSources": { "type": "object", "additionalProperties": { From ff9c2c35d7bb35670115571f6b2e6e3213ed1d27 Mon Sep 17 00:00:00 2001 From: Aria Desires Date: Fri, 13 Jun 2025 18:16:48 -0400 Subject: [PATCH 009/185] Support reading dependency-groups from pyproject.tomls with no project (#13742) (or legacy tool.uv.workspace). This cleaves out a dedicated SourcedDependencyGroups type based on RequiresDist but with only the DependencyGroup handling implemented. This allows `uv pip` to read `dependency-groups` from pyproject.tomls that only have that table defined, per PEP 735, and as implemented by `pip`. However we want our implementation to respect various uv features when they're available: * `tool.uv.sources` * `tool.uv.index` * `tool.uv.dependency-groups.mygroup.requires-python` (#13735) As such we want to opportunistically detect "as much as possible" while doing as little as possible when things are missing. The issue with the old RequiresDist path was that it fundamentally wanted to build the package, and if `[project]` was missing it would try to desperately run setuptools on the pyproject.toml to try to find metadata and make a hash of things. At the same time, the old code also put in a lot of effort to try to pretend that `uv pip` dependency-groups worked like `uv` dependency-groups with defaults and non-only semantics, only to separate them back out again. By explicitly separating them out, we confidently get the expected behaviour. Note that dependency-group support is still included in RequiresDist, as some `uv` paths still use it. It's unclear to me if those paths want this same treatment -- for now I conclude no. Fixes #13138 --- .../uv-distribution-types/src/annotation.rs | 20 +- crates/uv-distribution/src/lib.rs | 2 +- .../src/metadata/dependency_groups.rs | 208 ++++++++++++++++++ crates/uv-distribution/src/metadata/mod.rs | 2 + crates/uv-pep508/src/origin.rs | 4 +- crates/uv-requirements/src/source_tree.rs | 57 +---- crates/uv-requirements/src/specification.rs | 38 +--- crates/uv-workspace/src/workspace.rs | 45 ++++ crates/uv/src/commands/pip/install.rs | 1 + crates/uv/src/commands/pip/operations.rs | 52 ++++- crates/uv/tests/it/pip_compile.rs | 101 ++++++++- crates/uv/tests/it/pip_install.rs | 37 ++++ 12 files changed, 470 insertions(+), 97 deletions(-) create mode 100644 crates/uv-distribution/src/metadata/dependency_groups.rs diff --git a/crates/uv-distribution-types/src/annotation.rs b/crates/uv-distribution-types/src/annotation.rs index 673d23c17..398bcb6b4 100644 --- a/crates/uv-distribution-types/src/annotation.rs +++ b/crates/uv-distribution-types/src/annotation.rs @@ -26,7 +26,11 @@ impl std::fmt::Display for SourceAnnotation { write!(f, "{project_name} ({})", path.portable_display()) } RequirementOrigin::Group(path, project_name, group) => { - write!(f, "{project_name} ({}:{group})", path.portable_display()) + if let Some(project_name) = project_name { + write!(f, "{project_name} ({}:{group})", path.portable_display()) + } else { + write!(f, "({}:{group})", path.portable_display()) + } } RequirementOrigin::Workspace => { write!(f, "(workspace)") @@ -45,11 +49,15 @@ impl std::fmt::Display for SourceAnnotation { } RequirementOrigin::Group(path, project_name, group) => { // Group is not used for override - write!( - f, - "--override {project_name} ({}:{group})", - path.portable_display() - ) + if let Some(project_name) = project_name { + write!( + f, + "--override {project_name} ({}:{group})", + path.portable_display() + ) + } else { + write!(f, "--override ({}:{group})", path.portable_display()) + } } RequirementOrigin::Workspace => { write!(f, "--override (workspace)") diff --git a/crates/uv-distribution/src/lib.rs b/crates/uv-distribution/src/lib.rs index d7679a5fb..07958f715 100644 --- a/crates/uv-distribution/src/lib.rs +++ b/crates/uv-distribution/src/lib.rs @@ -4,7 +4,7 @@ pub use error::Error; pub use index::{BuiltWheelIndex, RegistryWheelIndex}; pub use metadata::{ ArchiveMetadata, BuildRequires, FlatRequiresDist, LoweredRequirement, LoweringError, Metadata, - MetadataError, RequiresDist, + MetadataError, RequiresDist, SourcedDependencyGroups, }; pub use reporter::Reporter; pub use source::prune; diff --git a/crates/uv-distribution/src/metadata/dependency_groups.rs b/crates/uv-distribution/src/metadata/dependency_groups.rs new file mode 100644 index 000000000..7fb69b516 --- /dev/null +++ b/crates/uv-distribution/src/metadata/dependency_groups.rs @@ -0,0 +1,208 @@ +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; + +use uv_configuration::SourceStrategy; +use uv_distribution_types::{IndexLocations, Requirement}; +use uv_normalize::{GroupName, PackageName}; +use uv_workspace::dependency_groups::FlatDependencyGroups; +use uv_workspace::pyproject::{Sources, ToolUvSources}; +use uv_workspace::{ + DiscoveryOptions, MemberDiscovery, VirtualProject, WorkspaceCache, WorkspaceError, +}; + +use crate::metadata::{GitWorkspaceMember, LoweredRequirement, MetadataError}; + +/// Like [`crate::RequiresDist`] but only supporting dependency-groups. +/// +/// PEP 735 says: +/// +/// > A pyproject.toml file with only `[dependency-groups]` and no other tables is valid. +/// +/// This is a special carveout to enable users to adopt dependency-groups without having +/// to learn about projects. It is supported by `pip install --group`, and thus interfaces +/// like `uv pip install --group` must also support it for interop and conformance. +/// +/// On paper this is trivial to support because dependency-groups are so self-contained +/// that they're basically a `requirements.txt` embedded within a pyproject.toml, so it's +/// fine to just grab that section and handle it independently. +/// +/// However several uv extensions make this complicated, notably, as of this writing: +/// +/// * tool.uv.sources +/// * tool.uv.index +/// +/// These fields may also be present in the pyproject.toml, and, critically, +/// may be defined and inherited in a parent workspace pyproject.toml. +/// +/// Therefore, we need to gracefully degrade from a full workspacey situation all +/// the way down to one of these stub pyproject.tomls the PEP defines. This is why +/// we avoid going through `RequiresDist` -- we don't want to muddy up the "compile a package" +/// logic with support for non-project/workspace pyproject.tomls, and we don't want to +/// muddy this logic up with setuptools fallback modes that `RequiresDist` wants. +/// +/// (We used to shove this feature into that path, and then we would see there's no metadata +/// and try to run setuptools to try to desperately find any metadata, and then error out.) +#[derive(Debug, Clone)] +pub struct SourcedDependencyGroups { + pub name: Option, + pub dependency_groups: BTreeMap>, +} + +impl SourcedDependencyGroups { + /// Lower by considering `tool.uv` in `pyproject.toml` if present, used for Git and directory + /// dependencies. + pub async fn from_virtual_project( + pyproject_path: &Path, + git_member: Option<&GitWorkspaceMember<'_>>, + locations: &IndexLocations, + source_strategy: SourceStrategy, + cache: &WorkspaceCache, + ) -> Result { + let discovery = DiscoveryOptions { + stop_discovery_at: git_member.map(|git_member| { + git_member + .fetch_root + .parent() + .expect("git checkout has a parent") + .to_path_buf() + }), + members: match source_strategy { + SourceStrategy::Enabled => MemberDiscovery::default(), + SourceStrategy::Disabled => MemberDiscovery::None, + }, + }; + + // The subsequent API takes an absolute path to the dir the pyproject is in + let empty = PathBuf::new(); + let absolute_pyproject_path = + std::path::absolute(pyproject_path).map_err(WorkspaceError::Normalize)?; + let project_dir = absolute_pyproject_path.parent().unwrap_or(&empty); + let project = VirtualProject::discover_defaulted(project_dir, &discovery, cache).await?; + + // Collect the dependency groups. + let dependency_groups = + FlatDependencyGroups::from_pyproject_toml(project.root(), project.pyproject_toml())?; + + // If sources/indexes are disabled we can just stop here + let SourceStrategy::Enabled = source_strategy else { + return Ok(Self { + name: project.project_name().cloned(), + dependency_groups: dependency_groups + .into_iter() + .map(|(name, group)| { + let requirements = group + .requirements + .into_iter() + .map(Requirement::from) + .collect(); + (name, requirements) + }) + .collect(), + }); + }; + + // Collect any `tool.uv.index` entries. + let empty = vec![]; + let project_indexes = project + .pyproject_toml() + .tool + .as_ref() + .and_then(|tool| tool.uv.as_ref()) + .and_then(|uv| uv.index.as_deref()) + .unwrap_or(&empty); + + // Collect any `tool.uv.sources` and `tool.uv.dev_dependencies` from `pyproject.toml`. + let empty = BTreeMap::default(); + let project_sources = project + .pyproject_toml() + .tool + .as_ref() + .and_then(|tool| tool.uv.as_ref()) + .and_then(|uv| uv.sources.as_ref()) + .map(ToolUvSources::inner) + .unwrap_or(&empty); + + // Now that we've resolved the dependency groups, we can validate that each source references + // a valid extra or group, if present. + Self::validate_sources(project_sources, &dependency_groups)?; + + // Lower the dependency groups. + let dependency_groups = dependency_groups + .into_iter() + .map(|(name, group)| { + let requirements = group + .requirements + .into_iter() + .flat_map(|requirement| { + let requirement_name = requirement.name.clone(); + let group = name.clone(); + let extra = None; + LoweredRequirement::from_requirement( + requirement, + project.project_name(), + project.root(), + project_sources, + project_indexes, + extra, + Some(&group), + locations, + project.workspace(), + git_member, + ) + .map(move |requirement| match requirement { + Ok(requirement) => Ok(requirement.into_inner()), + Err(err) => Err(MetadataError::GroupLoweringError( + group.clone(), + requirement_name.clone(), + Box::new(err), + )), + }) + }) + .collect::, _>>()?; + Ok::<(GroupName, Box<_>), MetadataError>((name, requirements)) + }) + .collect::, _>>()?; + + Ok(Self { + name: project.project_name().cloned(), + dependency_groups, + }) + } + + /// Validate the sources. + /// + /// If a source is requested with `group`, ensure that the relevant dependency is + /// present in the relevant `dependency-groups` section. + fn validate_sources( + sources: &BTreeMap, + dependency_groups: &FlatDependencyGroups, + ) -> Result<(), MetadataError> { + for (name, sources) in sources { + for source in sources.iter() { + if let Some(group) = source.group() { + // If the group doesn't exist at all, error. + let Some(flat_group) = dependency_groups.get(group) else { + return Err(MetadataError::MissingSourceGroup( + name.clone(), + group.clone(), + )); + }; + + // If there is no such requirement with the group, error. + if !flat_group + .requirements + .iter() + .any(|requirement| requirement.name == *name) + { + return Err(MetadataError::IncompleteSourceGroup( + name.clone(), + group.clone(), + )); + } + } + } + } + + Ok(()) + } +} diff --git a/crates/uv-distribution/src/metadata/mod.rs b/crates/uv-distribution/src/metadata/mod.rs index 85c55666e..a56a1c354 100644 --- a/crates/uv-distribution/src/metadata/mod.rs +++ b/crates/uv-distribution/src/metadata/mod.rs @@ -12,11 +12,13 @@ use uv_workspace::dependency_groups::DependencyGroupError; use uv_workspace::{WorkspaceCache, WorkspaceError}; pub use crate::metadata::build_requires::BuildRequires; +pub use crate::metadata::dependency_groups::SourcedDependencyGroups; pub use crate::metadata::lowering::LoweredRequirement; pub use crate::metadata::lowering::LoweringError; pub use crate::metadata::requires_dist::{FlatRequiresDist, RequiresDist}; mod build_requires; +mod dependency_groups; mod lowering; mod requires_dist; diff --git a/crates/uv-pep508/src/origin.rs b/crates/uv-pep508/src/origin.rs index 91a88f59a..4619e6f2e 100644 --- a/crates/uv-pep508/src/origin.rs +++ b/crates/uv-pep508/src/origin.rs @@ -12,8 +12,8 @@ pub enum RequirementOrigin { File(PathBuf), /// The requirement was provided via a local project (e.g., a `pyproject.toml` file). Project(PathBuf, PackageName), - /// The requirement was provided via a local project (e.g., a `pyproject.toml` file). - Group(PathBuf, PackageName, GroupName), + /// The requirement was provided via a local project's group (e.g., a `pyproject.toml` file). + Group(PathBuf, Option, GroupName), /// The requirement was provided via a workspace. Workspace, } diff --git a/crates/uv-requirements/src/source_tree.rs b/crates/uv-requirements/src/source_tree.rs index a540e4642..39fbe453b 100644 --- a/crates/uv-requirements/src/source_tree.rs +++ b/crates/uv-requirements/src/source_tree.rs @@ -1,13 +1,13 @@ -use std::path::{Path, PathBuf}; +use std::borrow::Cow; +use std::path::Path; use std::sync::Arc; -use std::{borrow::Cow, collections::BTreeMap}; use anyhow::{Context, Result}; use futures::TryStreamExt; use futures::stream::FuturesOrdered; use url::Url; -use uv_configuration::{DependencyGroups, ExtrasSpecification}; +use uv_configuration::ExtrasSpecification; use uv_distribution::{DistributionDatabase, FlatRequiresDist, Reporter, RequiresDist}; use uv_distribution_types::Requirement; use uv_distribution_types::{ @@ -37,8 +37,6 @@ pub struct SourceTreeResolution { pub struct SourceTreeResolver<'a, Context: BuildContext> { /// The extras to include when resolving requirements. extras: &'a ExtrasSpecification, - /// The groups to include when resolving requirements. - groups: &'a BTreeMap, /// The hash policy to enforce. hasher: &'a HashStrategy, /// The in-memory index for resolving dependencies. @@ -51,14 +49,12 @@ impl<'a, Context: BuildContext> SourceTreeResolver<'a, Context> { /// Instantiate a new [`SourceTreeResolver`] for a given set of `source_trees`. pub fn new( extras: &'a ExtrasSpecification, - groups: &'a BTreeMap, hasher: &'a HashStrategy, index: &'a InMemoryIndex, database: DistributionDatabase<'a, Context>, ) -> Self { Self { extras, - groups, hasher, index, database, @@ -101,46 +97,17 @@ impl<'a, Context: BuildContext> SourceTreeResolver<'a, Context> { let mut requirements = Vec::new(); - // Resolve any groups associated with this path - let default_groups = DependencyGroups::default(); - let groups = self.groups.get(path).unwrap_or(&default_groups); - // Flatten any transitive extras and include dependencies // (unless something like --only-group was passed) - if groups.prod() { - requirements.extend( - FlatRequiresDist::from_requirements(metadata.requires_dist, &metadata.name) - .into_iter() - .map(|requirement| Requirement { - origin: Some(origin.clone()), - marker: requirement.marker.simplify_extras(&extras), - ..requirement - }), - ); - } - - // Apply dependency-groups - for (group_name, group) in &metadata.dependency_groups { - if groups.contains(group_name) { - requirements.extend(group.iter().cloned().map(|group| Requirement { - origin: Some(RequirementOrigin::Group( - path.to_path_buf(), - metadata.name.clone(), - group_name.clone(), - )), - ..group - })); - } - } - // Complain if dependency groups are named that don't appear. - for name in groups.explicit_names() { - if !metadata.dependency_groups.contains_key(name) { - return Err(anyhow::anyhow!( - "The dependency group '{name}' was not found in the project: {}", - path.user_display() - )); - } - } + requirements.extend( + FlatRequiresDist::from_requirements(metadata.requires_dist, &metadata.name) + .into_iter() + .map(|requirement| Requirement { + origin: Some(origin.clone()), + marker: requirement.marker.simplify_extras(&extras), + ..requirement + }), + ); let requirements = requirements.into_boxed_slice(); let project = metadata.name; diff --git a/crates/uv-requirements/src/specification.rs b/crates/uv-requirements/src/specification.rs index a0b122de8..4c5741392 100644 --- a/crates/uv-requirements/src/specification.rs +++ b/crates/uv-requirements/src/specification.rs @@ -290,52 +290,18 @@ impl RequirementsSpecification { if !groups.is_empty() { let mut group_specs = BTreeMap::new(); for (path, groups) in groups { - // Conceptually pip `--group` flags just add the group referred to by the file. - // In uv semantics this would be like `--only-group`, however if you do this: - // - // uv pip install -r pyproject.toml --group pyproject.toml:foo - // - // We don't want to discard the package listed by `-r` in the way `--only-group` - // would. So we check to see if any other source wants to add this path, and use - // that to determine if we're doing `--group` or `--only-group` semantics. - // - // Note that it's fine if a file gets referred to multiple times by - // different-looking paths (like `./pyproject.toml` vs `pyproject.toml`). We're - // specifically trying to disambiguate in situations where the `--group` *happens* - // to match with an unrelated argument, and `--only-group` would be overzealous! - let source_exists_without_group = requirement_sources - .iter() - .any(|source| source.source_trees.contains(&path)); - let (group, only_group) = if source_exists_without_group { - (groups, Vec::new()) - } else { - (Vec::new(), groups) - }; let group_spec = DependencyGroups::from_args( false, false, false, - group, + Vec::new(), Vec::new(), false, - only_group, + groups, false, ); - - // If we're doing `--only-group` semantics it's because only `--group` flags referred - // to this file, and so we need to make sure to add it to the list of sources! - if !source_exists_without_group { - let source = Self::from_source( - &RequirementsSource::PyprojectToml(path.clone()), - client_builder, - ) - .await?; - requirement_sources.push(source); - } - group_specs.insert(path, group_spec); } - spec.groups = group_specs; } diff --git a/crates/uv-workspace/src/workspace.rs b/crates/uv-workspace/src/workspace.rs index ed99d3d30..1349d739c 100644 --- a/crates/uv-workspace/src/workspace.rs +++ b/crates/uv-workspace/src/workspace.rs @@ -1434,6 +1434,33 @@ impl VirtualProject { path: &Path, options: &DiscoveryOptions, cache: &WorkspaceCache, + ) -> Result { + Self::discover_impl(path, options, cache, false).await + } + + /// Equivalent to [`VirtualProject::discover`] but consider it acceptable for + /// both `[project]` and `[tool.uv.workspace]` to be missing. + /// + /// If they are, we act as if an empty `[tool.uv.workspace]` was found. + pub async fn discover_defaulted( + path: &Path, + options: &DiscoveryOptions, + cache: &WorkspaceCache, + ) -> Result { + Self::discover_impl(path, options, cache, true).await + } + + /// Find the current project or virtual workspace root, given the current directory. + /// + /// Similar to calling [`ProjectWorkspace::discover`] with a fallback to [`Workspace::discover`], + /// but avoids rereading the `pyproject.toml` (and relying on error-handling as control flow). + /// + /// This method requires an absolute path and panics otherwise. + async fn discover_impl( + path: &Path, + options: &DiscoveryOptions, + cache: &WorkspaceCache, + default_missing_workspace: bool, ) -> Result { assert!( path.is_absolute(), @@ -1497,6 +1524,24 @@ impl VirtualProject { ) .await?; + Ok(Self::NonProject(workspace)) + } else if default_missing_workspace { + // Otherwise it's a pyproject.toml that maybe contains dependency-groups + // that we want to treat like a project/workspace to handle those uniformly + let project_path = std::path::absolute(project_root) + .map_err(WorkspaceError::Normalize)? + .clone(); + + let workspace = Workspace::collect_members( + project_path, + ToolUvWorkspace::default(), + pyproject_toml, + None, + options, + cache, + ) + .await?; + Ok(Self::NonProject(workspace)) } else { Err(WorkspaceError::MissingProject(pyproject_path)) diff --git a/crates/uv/src/commands/pip/install.rs b/crates/uv/src/commands/pip/install.rs index eb9c1cd2b..5fc9a66f4 100644 --- a/crates/uv/src/commands/pip/install.rs +++ b/crates/uv/src/commands/pip/install.rs @@ -254,6 +254,7 @@ pub(crate) async fn pip_install( if reinstall.is_none() && upgrade.is_none() && source_trees.is_empty() + && groups.is_empty() && pylock.is_none() && matches!(modifications, Modifications::Sufficient) { diff --git a/crates/uv/src/commands/pip/operations.rs b/crates/uv/src/commands/pip/operations.rs index 907f79075..55ab2aa1b 100644 --- a/crates/uv/src/commands/pip/operations.rs +++ b/crates/uv/src/commands/pip/operations.rs @@ -8,7 +8,6 @@ use std::fmt::Write; use std::path::PathBuf; use std::sync::Arc; use tracing::debug; -use uv_tool::InstalledTools; use uv_cache::Cache; use uv_client::{BaseClientBuilder, RegistryClient}; @@ -17,9 +16,9 @@ use uv_configuration::{ ExtrasSpecification, Overrides, Reinstall, Upgrade, }; use uv_dispatch::BuildDispatch; -use uv_distribution::DistributionDatabase; +use uv_distribution::{DistributionDatabase, SourcedDependencyGroups}; use uv_distribution_types::{ - CachedDist, Diagnostic, InstalledDist, LocalDist, NameRequirementSpecification, + CachedDist, Diagnostic, InstalledDist, LocalDist, NameRequirementSpecification, Requirement, ResolutionDiagnostic, UnresolvedRequirement, UnresolvedRequirementSpecification, }; use uv_distribution_types::{ @@ -29,7 +28,7 @@ use uv_fs::Simplified; use uv_install_wheel::LinkMode; use uv_installer::{Plan, Planner, Preparer, SitePackages}; use uv_normalize::{GroupName, PackageName}; -use uv_pep508::MarkerEnvironment; +use uv_pep508::{MarkerEnvironment, RequirementOrigin}; use uv_platform_tags::Tags; use uv_pypi_types::{Conflicts, ResolverMarkerEnvironment}; use uv_python::{PythonEnvironment, PythonInstallation}; @@ -41,7 +40,8 @@ use uv_resolver::{ DependencyMode, Exclusions, FlatIndex, InMemoryIndex, Manifest, Options, Preference, Preferences, PythonRequirement, Resolver, ResolverEnvironment, ResolverOutput, }; -use uv_types::{HashStrategy, InFlight, InstalledPackagesProvider}; +use uv_tool::InstalledTools; +use uv_types::{BuildContext, HashStrategy, InFlight, InstalledPackagesProvider}; use uv_warnings::warn_user; use crate::commands::pip::loggers::{DefaultInstallLogger, InstallLogger, ResolveLogger}; @@ -166,7 +166,6 @@ pub(crate) async fn resolve( if !source_trees.is_empty() { let resolutions = SourceTreeResolver::new( extras, - groups, hasher, index, DistributionDatabase::new(client, build_dispatch, concurrency.downloads), @@ -212,6 +211,47 @@ pub(crate) async fn resolve( ); } + for (pyproject_path, groups) in groups { + let metadata = SourcedDependencyGroups::from_virtual_project( + pyproject_path, + None, + build_dispatch.locations(), + build_dispatch.sources(), + build_dispatch.workspace_cache(), + ) + .await + .map_err(|e| { + anyhow!( + "Failed to read dependency groups from: {}\n{}", + pyproject_path.display(), + e + ) + })?; + + // Complain if dependency groups are named that don't appear. + for name in groups.explicit_names() { + if !metadata.dependency_groups.contains_key(name) { + return Err(anyhow!( + "The dependency group '{name}' was not found in the project: {}", + pyproject_path.user_display() + ))?; + } + } + // Apply dependency-groups + for (group_name, group) in &metadata.dependency_groups { + if groups.contains(group_name) { + requirements.extend(group.iter().cloned().map(|group| Requirement { + origin: Some(RequirementOrigin::Group( + pyproject_path.clone(), + metadata.name.clone(), + group_name.clone(), + )), + ..group + })); + } + } + } + requirements }; diff --git a/crates/uv/tests/it/pip_compile.rs b/crates/uv/tests/it/pip_compile.rs index efb51e47d..49e78a5c9 100644 --- a/crates/uv/tests/it/pip_compile.rs +++ b/crates/uv/tests/it/pip_compile.rs @@ -15783,7 +15783,106 @@ fn invalid_group() -> Result<()> { } #[test] -fn project_and_group() -> Result<()> { +fn project_and_group_workspace_inherit() -> Result<()> { + // Checking that --project is handled properly with --group + fn new_context() -> Result { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "myproject" + version = "0.1.0" + requires-python = ">=3.12" + + [tool.uv.workspace] + members = ["packages/*"] + + [tool.uv.sources] + pytest = { workspace = true } + "#, + )?; + + let subdir = context.temp_dir.child("packages"); + subdir.create_dir_all()?; + + let pytest_dir = subdir.child("pytest"); + pytest_dir.create_dir_all()?; + let pytest_toml = pytest_dir.child("pyproject.toml"); + pytest_toml.write_str( + r#" + [project] + name = "pytest" + version = "4.0.0" + requires-python = ">=3.12" + "#, + )?; + + let sniffio_dir = subdir.child("sniffio"); + sniffio_dir.create_dir_all()?; + let sniffio_toml = sniffio_dir.child("pyproject.toml"); + sniffio_toml.write_str( + r#" + [project] + name = "sniffio" + version = "1.3.1" + requires-python = ">=3.12" + "#, + )?; + + let subproject_dir = subdir.child("mysubproject"); + subproject_dir.create_dir_all()?; + let subproject_toml = subproject_dir.child("pyproject.toml"); + subproject_toml.write_str( + r#" + [project] + name = "mysubproject" + version = "0.1.0" + requires-python = ">=3.12" + + [tool.uv.sources] + sniffio = { workspace = true } + + [dependency-groups] + foo = ["iniconfig", "anyio", "sniffio", "pytest"] + "#, + )?; + + Ok(context) + } + + // Check that the workspace's sources are discovered and consulted + let context = new_context()?; + uv_snapshot!(context.filters(), context.pip_compile() + .arg("--group").arg("packages/mysubproject/pyproject.toml:foo"), @r" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] --group packages/mysubproject/pyproject.toml:foo + anyio==4.3.0 + # via mysubproject (packages/mysubproject/pyproject.toml:foo) + idna==3.6 + # via anyio + iniconfig==2.0.0 + # via mysubproject (packages/mysubproject/pyproject.toml:foo) + pytest @ file://[TEMP_DIR]/packages/pytest + # via mysubproject (packages/mysubproject/pyproject.toml:foo) + sniffio @ file://[TEMP_DIR]/packages/sniffio + # via + # mysubproject (packages/mysubproject/pyproject.toml:foo) + # anyio + + ----- stderr ----- + Resolved 5 packages in [TIME] + "); + + Ok(()) +} + +#[test] +fn project_and_group_workspace() -> Result<()> { // Checking that --project is handled properly with --group fn new_context() -> Result { let context = TestContext::new("3.12"); diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index 815cbac1f..569edf00e 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -9632,6 +9632,43 @@ fn dependency_group() -> Result<()> { Ok(()) } +#[test] +fn virtual_dependency_group() -> Result<()> { + // testing basic `uv pip install --group` functionality + // when the pyproject.toml is virtual + fn new_context() -> Result { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [dependency-groups] + foo = ["sortedcontainers"] + bar = ["iniconfig"] + dev = ["sniffio"] + "#, + )?; + Ok(context) + } + + // 'bar' using path sugar + let context = new_context()?; + uv_snapshot!(context.filters(), context.pip_install() + .arg("--group").arg("bar"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + iniconfig==2.0.0 + "); + + Ok(()) +} + #[test] fn many_pyproject_group() -> Result<()> { // `uv pip install --group` tests with multiple projects From 15256722d8d4d2651facdeed03cc103abbfbbf45 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 15 Jun 2025 21:36:58 -0400 Subject: [PATCH 010/185] Update Rust crate clap to v4.5.40 (#14047) --- Cargo.lock | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6864b4319..bda679f9b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -601,9 +601,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.39" +version = "4.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f" +checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f" dependencies = [ "clap_builder", "clap_derive", @@ -611,9 +611,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.39" +version = "4.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51" +checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e" dependencies = [ "anstream", "anstyle", @@ -654,9 +654,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.32" +version = "4.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" +checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce" dependencies = [ "heck", "proc-macro2", @@ -1115,7 +1115,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -1942,7 +1942,7 @@ checksum = "e19b23d53f35ce9f56aebc7d1bb4e6ac1e9c0db7ac85c8d1760c04379edced37" dependencies = [ "hermit-abi 0.4.0", "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -2002,7 +2002,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -2886,7 +2886,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -3335,7 +3335,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -3348,7 +3348,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.9.2", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -3929,7 +3929,7 @@ dependencies = [ "getrandom 0.3.1", "once_cell", "rustix 1.0.7", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] From ce16a0fc9b1da5fa6bda5cc0a53dca2f42b5c305 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 15 Jun 2025 21:37:16 -0400 Subject: [PATCH 011/185] Update Rust crate boxcar to v0.2.13 (#14046) --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bda679f9b..b619f1e7c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -402,9 +402,9 @@ dependencies = [ [[package]] name = "boxcar" -version = "0.2.12" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66bb12751a83493ef4b8da1120451a262554e216a247f14b48cb5e8fe7ed8bdf" +checksum = "26c4925bc979b677330a8c7fe7a8c94af2dbb4a2d37b4a20a80d884400f46baa" [[package]] name = "bstr" From 3c00d6d7dfd3ca61d239ccae43b59b3240168469 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 15 Jun 2025 21:37:22 -0400 Subject: [PATCH 012/185] Update Rust crate memchr to v2.7.5 (#14048) --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b619f1e7c..ee3484030 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2228,9 +2228,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "memmap2" From f0e0ad4d091f0f3ad790b150376276501f201e3a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 15 Jun 2025 21:37:31 -0400 Subject: [PATCH 013/185] Update Rust crate smallvec to v1.15.1 (#14050) --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ee3484030..f29ef5e8a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3733,9 +3733,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.15.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "smawk" From 96cb90a1fdcc075ff845c54257ec9094fa650ed3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 15 Jun 2025 21:41:31 -0400 Subject: [PATCH 014/185] Update Rust crate toml to v0.8.23 (#14052) --- Cargo.lock | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f29ef5e8a..d1affa28f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3605,9 +3605,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" dependencies = [ "serde", ] @@ -4227,9 +4227,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.22" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", "serde_spanned", @@ -4239,18 +4239,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.9" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.26" +version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", "serde", @@ -4262,9 +4262,9 @@ dependencies = [ [[package]] name = "toml_write" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "tower" @@ -6726,9 +6726,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" -version = "0.7.7" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cb8234a863ea0e8cd7284fcdd4f145233eb00fee02bbdd9861aec44e6477bc5" +checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd" dependencies = [ "memchr", ] From 59f1b4bee4b11cd3f075ca711f76ee333f014294 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 15 Jun 2025 21:41:54 -0400 Subject: [PATCH 015/185] Update Rust crate syn to v2.0.103 (#14051) --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d1affa28f..839e3a116 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3848,9 +3848,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.101" +version = "2.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +checksum = "e4307e30089d6fd6aff212f2da3a1f9e32f3223b1f010fb09b7c95f90f3ca1e8" dependencies = [ "proc-macro2", "quote", From f38e96bddd5e1319783a0d095b75975e9a355a57 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 15 Jun 2025 21:47:33 -0400 Subject: [PATCH 016/185] Update actions/setup-python digest to a26af69 (#14045) --- .github/workflows/build-binaries.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml index 5f42bdf9c..94a16c0b0 100644 --- a/.github/workflows/build-binaries.yml +++ b/.github/workflows/build-binaries.yml @@ -704,7 +704,7 @@ jobs: steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - - uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: ${{ env.PYTHON_VERSION }} - name: "Prep README.md" From 4b5da24fe04c3048f0afe2fa46b6a870771d57c6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 16 Jun 2025 02:04:46 +0000 Subject: [PATCH 017/185] Update Rust crate unicode-width to v0.2.1 (#14054) --- Cargo.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 839e3a116..b7f85deca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -765,7 +765,7 @@ dependencies = [ "encode_unicode", "libc", "once_cell", - "unicode-width 0.2.0", + "unicode-width 0.2.1", "windows-sys 0.59.0", ] @@ -1891,7 +1891,7 @@ dependencies = [ "console", "number_prefix", "portable-atomic", - "unicode-width 0.2.0", + "unicode-width 0.2.1", "web-time", ] @@ -4010,7 +4010,7 @@ checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" dependencies = [ "smawk", "unicode-linebreak", - "unicode-width 0.2.0", + "unicode-width 0.2.1", ] [[package]] @@ -4516,9 +4516,9 @@ checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "unicode-width" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" [[package]] name = "unsafe-libyaml" @@ -4667,7 +4667,7 @@ dependencies = [ "tracing-durations-export", "tracing-subscriber", "tracing-tree", - "unicode-width 0.2.0", + "unicode-width 0.2.1", "url", "uv-auth", "uv-build-backend", @@ -5466,7 +5466,7 @@ dependencies = [ "rkyv", "serde", "tracing", - "unicode-width 0.2.0", + "unicode-width 0.2.1", "unscanny", "version-ranges", ] @@ -5489,7 +5489,7 @@ dependencies = [ "thiserror 2.0.12", "tracing", "tracing-test", - "unicode-width 0.2.0", + "unicode-width 0.2.1", "url", "uv-fs", "uv-normalize", From 4d104dd004c03f831e04a92a06ffeb747af4eaaf Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 16 Jun 2025 02:05:34 +0000 Subject: [PATCH 018/185] Update Rust crate windows to v0.61.3 (#14055) --- crates/uv-trampoline/Cargo.lock | 34 +++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/crates/uv-trampoline/Cargo.lock b/crates/uv-trampoline/Cargo.lock index 89a7c5979..37edf9ede 100644 --- a/crates/uv-trampoline/Cargo.lock +++ b/crates/uv-trampoline/Cargo.lock @@ -100,9 +100,9 @@ dependencies = [ [[package]] name = "windows" -version = "0.61.1" +version = "0.61.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ "windows-collections", "windows-core", @@ -122,9 +122,9 @@ dependencies = [ [[package]] name = "windows-core" -version = "0.61.0" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement", "windows-interface", @@ -135,12 +135,13 @@ dependencies = [ [[package]] name = "windows-future" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a1d6bbefcb7b60acd19828e1bc965da6fcf18a7e39490c5f8be71e54a19ba32" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ "windows-core", "windows-link", + "windows-threading", ] [[package]] @@ -167,9 +168,9 @@ dependencies = [ [[package]] name = "windows-link" -version = "0.1.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" [[package]] name = "windows-numerics" @@ -183,18 +184,27 @@ dependencies = [ [[package]] name = "windows-result" -version = "0.3.2" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ "windows-link", ] [[package]] name = "windows-strings" -version = "0.4.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" dependencies = [ "windows-link", ] From a0ea520fe3676b6ed6bf212fa29eafa23ea11d49 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 16 Jun 2025 02:05:57 +0000 Subject: [PATCH 019/185] Update taiki-e/install-action action to v2.52.8 (#14056) --- .github/workflows/ci.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e29d8743c..ffc430330 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -187,7 +187,7 @@ jobs: steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: "Install cargo shear" - uses: taiki-e/install-action@735e5933943122c5ac182670a935f54a949265c1 # v2.52.4 + uses: taiki-e/install-action@7b20dfd705618832f20d29066e34aa2f2f6194c2 # v2.52.8 with: tool: cargo-shear - run: cargo shear @@ -217,7 +217,7 @@ jobs: run: uv python install - name: "Install cargo nextest" - uses: taiki-e/install-action@735e5933943122c5ac182670a935f54a949265c1 # v2.52.4 + uses: taiki-e/install-action@7b20dfd705618832f20d29066e34aa2f2f6194c2 # v2.52.8 with: tool: cargo-nextest @@ -249,7 +249,7 @@ jobs: run: uv python install - name: "Install cargo nextest" - uses: taiki-e/install-action@735e5933943122c5ac182670a935f54a949265c1 # v2.52.4 + uses: taiki-e/install-action@7b20dfd705618832f20d29066e34aa2f2f6194c2 # v2.52.8 with: tool: cargo-nextest @@ -291,7 +291,7 @@ jobs: run: rustup show - name: "Install cargo nextest" - uses: taiki-e/install-action@735e5933943122c5ac182670a935f54a949265c1 # v2.52.4 + uses: taiki-e/install-action@7b20dfd705618832f20d29066e34aa2f2f6194c2 # v2.52.8 with: tool: cargo-nextest @@ -394,7 +394,7 @@ jobs: rustup component add rust-src --target ${{ matrix.target-arch }}-pc-windows-msvc - name: "Install cargo-bloat" - uses: taiki-e/install-action@735e5933943122c5ac182670a935f54a949265c1 # v2.52.4 + uses: taiki-e/install-action@7b20dfd705618832f20d29066e34aa2f2f6194c2 # v2.52.8 with: tool: cargo-bloat @@ -2310,7 +2310,7 @@ jobs: run: rustup show - name: "Install codspeed" - uses: taiki-e/install-action@735e5933943122c5ac182670a935f54a949265c1 # v2.52.4 + uses: taiki-e/install-action@7b20dfd705618832f20d29066e34aa2f2f6194c2 # v2.52.8 with: tool: cargo-codspeed @@ -2347,7 +2347,7 @@ jobs: run: rustup show - name: "Install codspeed" - uses: taiki-e/install-action@735e5933943122c5ac182670a935f54a949265c1 # v2.52.4 + uses: taiki-e/install-action@7b20dfd705618832f20d29066e34aa2f2f6194c2 # v2.52.8 with: tool: cargo-codspeed From ba6413f81fcb3247aad31f8cc6e520f2aaa4ceb5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 16 Jun 2025 02:17:30 +0000 Subject: [PATCH 020/185] Update actions/attest-build-provenance action to v2.4.0 (#14058) --- .github/workflows/build-docker.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml index 520800991..1488774b0 100644 --- a/.github/workflows/build-docker.yml +++ b/.github/workflows/build-docker.yml @@ -295,7 +295,7 @@ jobs: annotations: ${{ steps.meta.outputs.annotations }} - name: Generate artifact attestation - uses: actions/attest-build-provenance@db473fddc028af60658334401dc6fa3ffd8669fd # v2.3.0 + uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0 with: subject-name: ${{ env.UV_BASE_IMG }} subject-digest: ${{ steps.build-and-push.outputs.digest }} @@ -384,7 +384,7 @@ jobs: echo "digest=${digest}" >> "$GITHUB_OUTPUT" - name: Generate artifact attestation - uses: actions/attest-build-provenance@db473fddc028af60658334401dc6fa3ffd8669fd # v2.3.0 + uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0 with: subject-name: ${{ env.UV_BASE_IMG }} subject-digest: ${{ steps.manifest-digest.outputs.digest }} From 67c0f93e375b3fdb1e3c94909c53cfe47209e4de Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 16 Jun 2025 02:27:03 +0000 Subject: [PATCH 021/185] Update actions/checkout action to v4.2.2 (#14059) --- .github/workflows/build-binaries.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml index 94a16c0b0..a92db0467 100644 --- a/.github/workflows/build-binaries.yml +++ b/.github/workflows/build-binaries.yml @@ -703,7 +703,7 @@ jobs: arch: riscv64 steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: ${{ env.PYTHON_VERSION }} From 87827b6d8251884004455d2d50df40e62a8ba243 Mon Sep 17 00:00:00 2001 From: Frazer McLean Date: Mon, 16 Jun 2025 04:37:40 +0200 Subject: [PATCH 022/185] Fix implied `platform_machine` marker for `win_amd64` platform tag (#14041) ## Summary Fixes #14040 ## Test Plan Added a test using required-environments --- .../src/prioritized_distribution.rs | 4 +- crates/uv/tests/it/lock.rs | 75 +++++++++++++++++++ 2 files changed, 77 insertions(+), 2 deletions(-) diff --git a/crates/uv-distribution-types/src/prioritized_distribution.rs b/crates/uv-distribution-types/src/prioritized_distribution.rs index e3da41a67..52ac2fbd1 100644 --- a/crates/uv-distribution-types/src/prioritized_distribution.rs +++ b/crates/uv-distribution-types/src/prioritized_distribution.rs @@ -831,7 +831,7 @@ pub fn implied_markers(filename: &WheelFilename) -> MarkerTree { tag_marker.and(MarkerTree::expression(MarkerExpression::String { key: MarkerValueString::PlatformMachine, operator: MarkerOperator::Equal, - value: arcstr::literal!("x86_64"), + value: arcstr::literal!("AMD64"), })); marker.or(tag_marker); } @@ -925,7 +925,7 @@ mod tests { ); assert_markers( "numpy-2.2.1-cp313-cp313t-win_amd64.whl", - "sys_platform == 'win32' and platform_machine == 'x86_64'", + "sys_platform == 'win32' and platform_machine == 'AMD64'", ); assert_markers( "numpy-2.2.1-cp313-cp313t-win_arm64.whl", diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index 260211269..e9615538e 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -27495,6 +27495,81 @@ fn windows_arm() -> Result<()> { Ok(()) } +#[test] +fn windows_amd64_required() -> Result<()> { + let context = TestContext::new("3.12").with_exclude_newer("2025-01-30T00:00:00Z"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "pywin32-prj" + version = "0.1.0" + requires-python = "~=3.12.0" + dependencies = ["pywin32; sys_platform == 'win32'"] + + [tool.uv] + required-environments = [ + "sys_platform == 'win32' and platform_machine == 'x86'", + "sys_platform == 'win32' and platform_machine == 'AMD64'", + ] + "#, + )?; + + uv_snapshot!(context.filters(), context.lock(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + "###); + + let lock = context.read("uv.lock"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r#" + version = 1 + revision = 2 + requires-python = ">=3.12.[X], <3.13" + required-markers = [ + "platform_machine == 'x86' and sys_platform == 'win32'", + "platform_machine == 'AMD64' and sys_platform == 'win32'", + ] + + [options] + exclude-newer = "2025-01-30T00:00:00Z" + + [[package]] + name = "pywin32" + version = "308" + source = { registry = "https://pypi.org/simple" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/00/7c/d00d6bdd96de4344e06c4afbf218bc86b54436a94c01c71a8701f613aa56/pywin32-308-cp312-cp312-win32.whl", hash = "sha256:587f3e19696f4bf96fde9d8a57cec74a57021ad5f204c9e627e15c33ff568897", size = 5939729, upload-time = "2024-10-12T20:42:12.001Z" }, + { url = "https://files.pythonhosted.org/packages/21/27/0c8811fbc3ca188f93b5354e7c286eb91f80a53afa4e11007ef661afa746/pywin32-308-cp312-cp312-win_amd64.whl", hash = "sha256:00b3e11ef09ede56c6a43c71f2d31857cf7c54b0ab6e78ac659497abd2834f47", size = 6543015, upload-time = "2024-10-12T20:42:14.044Z" }, + { url = "https://files.pythonhosted.org/packages/9d/0f/d40f8373608caed2255781a3ad9a51d03a594a1248cd632d6a298daca693/pywin32-308-cp312-cp312-win_arm64.whl", hash = "sha256:9b4de86c8d909aed15b7011182c8cab38c8850de36e6afb1f0db22b8959e3091", size = 7976033, upload-time = "2024-10-12T20:42:16.215Z" }, + ] + + [[package]] + name = "pywin32-prj" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "pywin32", marker = "sys_platform == 'win32'" }, + ] + + [package.metadata] + requires-dist = [{ name = "pywin32", marker = "sys_platform == 'win32'" }] + "# + ); + }); + + Ok(()) +} + #[test] fn lock_empty_extra() -> Result<()> { let context = TestContext::new("3.12"); From 9d0d61213107f74e190dbb71f62970f26c95a499 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 16 Jun 2025 09:37:38 +0200 Subject: [PATCH 023/185] Update Rust crate which to v8 (#14066) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [which](https://redirect.github.com/harryfei/which-rs) | workspace.dependencies | major | `7.0.0` -> `8.0.0` | --- > [!WARNING] > Some dependencies could not be looked up. Check the Dependency Dashboard for more information. --- ### Release Notes
harryfei/which-rs (which) ### [`v8.0.0`](https://redirect.github.com/harryfei/which-rs/blob/HEAD/CHANGELOG.md#800) [Compare Source](https://redirect.github.com/harryfei/which-rs/compare/7.0.3...8.0.0) - Add new `Sys` trait to allow abstracting over the underlying filesystem. Particularly useful for `wasm32-unknown-unknown` targets. Thanks [@​dsherret](https://redirect.github.com/dsherret) for this contribution to which! - Add more debug level tracing for otherwise silent I/O errors. - Call the `NonFatalHandler` in more places to catch previously ignored I/O errors. - Remove use of the `either` dependency.
--- ### Configuration 📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/astral-sh/uv). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 23 +++++++++++------------ Cargo.toml | 2 +- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b7f85deca..8e24c8ba9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -738,7 +738,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" dependencies = [ "lazy_static", - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] @@ -1115,7 +1115,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1942,7 +1942,7 @@ checksum = "e19b23d53f35ce9f56aebc7d1bb4e6ac1e9c0db7ac85c8d1760c04379edced37" dependencies = [ "hermit-abi 0.4.0", "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2002,7 +2002,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2886,7 +2886,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3335,7 +3335,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3348,7 +3348,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.9.2", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3929,7 +3929,7 @@ dependencies = [ "getrandom 0.3.1", "once_cell", "rustix 1.0.7", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -6234,11 +6234,10 @@ checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" [[package]] name = "which" -version = "7.0.3" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d643ce3fd3e5b54854602a080f34fb10ab75e0b813ee32d00ca2b44fa74762" +checksum = "d3fabb953106c3c8eea8306e4393700d7657561cb43122571b172bbfb7c7ba1d" dependencies = [ - "either", "env_home", "regex", "rustix 1.0.7", @@ -6284,7 +6283,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 55bbde3e9..fa5ecad2f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -183,7 +183,7 @@ unscanny = { version = "0.1.0" } url = { version = "2.5.2", features = ["serde"] } version-ranges = { git = "https://github.com/astral-sh/pubgrub", rev = "06ec5a5f59ffaeb6cf5079c6cb184467da06c9db" } walkdir = { version = "2.5.0" } -which = { version = "7.0.0", features = ["regex"] } +which = { version = "8.0.0", features = ["regex"] } windows = { version = "0.59.0", features = ["Win32_Storage_FileSystem"] } windows-core = { version = "0.59.0" } windows-registry = { version = "0.5.0" } From 5beeda7cdc88bcf44976358b59b8e0758f866d4b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 16 Jun 2025 07:42:01 +0000 Subject: [PATCH 024/185] Update acj/freebsd-firecracker-action action to v0.5.0 (#14057) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [acj/freebsd-firecracker-action](https://redirect.github.com/acj/freebsd-firecracker-action) | action | minor | `v0.4.2` -> `v0.5.0` | --- > [!WARNING] > Some dependencies could not be looked up. Check the Dependency Dashboard for more information. --- ### Release Notes
acj/freebsd-firecracker-action (acj/freebsd-firecracker-action) ### [`v0.5.0`](https://redirect.github.com/acj/freebsd-firecracker-action/releases/tag/v0.5.0) [Compare Source](https://redirect.github.com/acj/freebsd-firecracker-action/compare/v0.4.2...v0.5.0) Changes: - Add `disk-size` option to control the size of the VM's disk and root filesystem - Retired obsolete workaround that disabled TCP segmentation offload (TSO)
--- ### Configuration 📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/astral-sh/uv). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ffc430330..c2be5cd62 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -712,7 +712,7 @@ jobs: cross build --target x86_64-unknown-freebsd - name: Test in Firecracker VM - uses: acj/freebsd-firecracker-action@d548632daa4f81a142a054c9829408e659350eb0 # v0.4.2 + uses: acj/freebsd-firecracker-action@6c57bda7113c2f137ef00d54512d61ae9d64365b # v0.5.0 with: verbose: false checkout: false From 77ec5f9b1733b183e7a5b9a5a15918aa6561a5e9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 16 Jun 2025 09:48:19 +0000 Subject: [PATCH 025/185] Update actions/setup-python action to v5.6.0 (#14060) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [actions/setup-python](https://redirect.github.com/actions/setup-python) | action | minor | `v5` -> `v5.6.0` | --- > [!WARNING] > Some dependencies could not be looked up. Check the Dependency Dashboard for more information. --- ### Release Notes
actions/setup-python (actions/setup-python) ### [`v5.6.0`](https://redirect.github.com/actions/setup-python/releases/tag/v5.6.0) [Compare Source](https://redirect.github.com/actions/setup-python/compare/v5.5.0...v5.6.0) ##### What's Changed - Workflow updates related to Ubuntu 20.04 by [@​aparnajyothi-y](https://redirect.github.com/aparnajyothi-y) in [https://github.com/actions/setup-python/pull/1065](https://redirect.github.com/actions/setup-python/pull/1065) - Fix for Candidate Not Iterable Error by [@​aparnajyothi-y](https://redirect.github.com/aparnajyothi-y) in [https://github.com/actions/setup-python/pull/1082](https://redirect.github.com/actions/setup-python/pull/1082) - Upgrade semver and [@​types/semver](https://redirect.github.com/types/semver) by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/actions/setup-python/pull/1091](https://redirect.github.com/actions/setup-python/pull/1091) - Upgrade prettier from 2.8.8 to 3.5.3 by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/actions/setup-python/pull/1046](https://redirect.github.com/actions/setup-python/pull/1046) - Upgrade ts-jest from 29.1.2 to 29.3.2 by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/actions/setup-python/pull/1081](https://redirect.github.com/actions/setup-python/pull/1081) **Full Changelog**: https://github.com/actions/setup-python/compare/v5...v5.6.0 ### [`v5.5.0`](https://redirect.github.com/actions/setup-python/releases/tag/v5.5.0) [Compare Source](https://redirect.github.com/actions/setup-python/compare/v5.4.0...v5.5.0) ##### What's Changed ##### Enhancements: - Support free threaded Python versions like '3.13t' by [@​colesbury](https://redirect.github.com/colesbury) in [https://github.com/actions/setup-python/pull/973](https://redirect.github.com/actions/setup-python/pull/973) - Enhance Workflows: Include ubuntu-arm runners, Add e2e Testing for free threaded and Upgrade [@​action/cache](https://redirect.github.com/action/cache) from 4.0.0 to 4.0.3 by [@​priya-kinthali](https://redirect.github.com/priya-kinthali) in [https://github.com/actions/setup-python/pull/1056](https://redirect.github.com/actions/setup-python/pull/1056) - Add support for .tool-versions file in setup-python by [@​mahabaleshwars](https://redirect.github.com/mahabaleshwars) in [https://github.com/actions/setup-python/pull/1043](https://redirect.github.com/actions/setup-python/pull/1043) ##### Bug fixes: - Fix architecture for pypy on Linux ARM64 by [@​mayeut](https://redirect.github.com/mayeut) in [https://github.com/actions/setup-python/pull/1011](https://redirect.github.com/actions/setup-python/pull/1011) This update maps arm64 to aarch64 for Linux ARM64 PyPy installations. ##### Dependency updates: - Upgrade [@​vercel/ncc](https://redirect.github.com/vercel/ncc) from 0.38.1 to 0.38.3 by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/actions/setup-python/pull/1016](https://redirect.github.com/actions/setup-python/pull/1016) - Upgrade [@​actions/glob](https://redirect.github.com/actions/glob) from 0.4.0 to 0.5.0 by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/actions/setup-python/pull/1015](https://redirect.github.com/actions/setup-python/pull/1015) ##### New Contributors - [@​colesbury](https://redirect.github.com/colesbury) made their first contribution in [https://github.com/actions/setup-python/pull/973](https://redirect.github.com/actions/setup-python/pull/973) - [@​mahabaleshwars](https://redirect.github.com/mahabaleshwars) made their first contribution in [https://github.com/actions/setup-python/pull/1043](https://redirect.github.com/actions/setup-python/pull/1043) **Full Changelog**: https://github.com/actions/setup-python/compare/v5...v5.5.0 ### [`v5.4.0`](https://redirect.github.com/actions/setup-python/releases/tag/v5.4.0) [Compare Source](https://redirect.github.com/actions/setup-python/compare/v5.3.0...v5.4.0) ##### What's Changed ##### Enhancements: - Update cache error message by [@​aparnajyothi-y](https://redirect.github.com/aparnajyothi-y) in [https://github.com/actions/setup-python/pull/968](https://redirect.github.com/actions/setup-python/pull/968) - Enhance Workflows: Add Ubuntu-24, Remove Python 3.8 by [@​priya-kinthali](https://redirect.github.com/priya-kinthali) in [https://github.com/actions/setup-python/pull/985](https://redirect.github.com/actions/setup-python/pull/985) - Configure Dependabot settings by [@​HarithaVattikuti](https://redirect.github.com/HarithaVattikuti) in [https://github.com/actions/setup-python/pull/1008](https://redirect.github.com/actions/setup-python/pull/1008) ##### Documentation changes: - Readme update - recommended permissions by [@​benwells](https://redirect.github.com/benwells) in [https://github.com/actions/setup-python/pull/1009](https://redirect.github.com/actions/setup-python/pull/1009) - Improve Advanced Usage examples by [@​lrq3000](https://redirect.github.com/lrq3000) in [https://github.com/actions/setup-python/pull/645](https://redirect.github.com/actions/setup-python/pull/645) ##### Dependency updates: - Upgrade `undici` from 5.28.4 to 5.28.5 by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/actions/setup-python/pull/1012](https://redirect.github.com/actions/setup-python/pull/1012) - Upgrade `urllib3` from 1.25.9 to 1.26.19 in /**tests**/data by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/actions/setup-python/pull/895](https://redirect.github.com/actions/setup-python/pull/895) - Upgrade `actions/publish-immutable-action` from 0.0.3 to 0.0.4 by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/actions/setup-python/pull/1014](https://redirect.github.com/actions/setup-python/pull/1014) - Upgrade `@actions/http-client` from 2.2.1 to 2.2.3 by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/actions/setup-python/pull/1020](https://redirect.github.com/actions/setup-python/pull/1020) - Upgrade `requests` from 2.24.0 to 2.32.2 in /**tests**/data by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/actions/setup-python/pull/1019](https://redirect.github.com/actions/setup-python/pull/1019) - Upgrade `@actions/cache` to `^4.0.0` by [@​priyagupta108](https://redirect.github.com/priyagupta108) in [https://github.com/actions/setup-python/pull/1007](https://redirect.github.com/actions/setup-python/pull/1007) ##### New Contributors - [@​benwells](https://redirect.github.com/benwells) made their first contribution in [https://github.com/actions/setup-python/pull/1009](https://redirect.github.com/actions/setup-python/pull/1009) - [@​HarithaVattikuti](https://redirect.github.com/HarithaVattikuti) made their first contribution in [https://github.com/actions/setup-python/pull/1008](https://redirect.github.com/actions/setup-python/pull/1008) - [@​lrq3000](https://redirect.github.com/lrq3000) made their first contribution in [https://github.com/actions/setup-python/pull/645](https://redirect.github.com/actions/setup-python/pull/645) **Full Changelog**: https://github.com/actions/setup-python/compare/v5...v5.4.0 ### [`v5.3.0`](https://redirect.github.com/actions/setup-python/releases/tag/v5.3.0) [Compare Source](https://redirect.github.com/actions/setup-python/compare/v5.2.0...v5.3.0) #### What's Changed - Add workflow file for publishing releases to immutable action package by [@​Jcambass](https://redirect.github.com/Jcambass) in [https://github.com/actions/setup-python/pull/941](https://redirect.github.com/actions/setup-python/pull/941) - Upgrade IA publish by [@​Jcambass](https://redirect.github.com/Jcambass) in [https://github.com/actions/setup-python/pull/943](https://redirect.github.com/actions/setup-python/pull/943) ##### Bug Fixes: - Normalise Line Endings to Ensure Cross-Platform Consistency by [@​priya-kinthali](https://redirect.github.com/priya-kinthali) in [https://github.com/actions/setup-python/pull/938](https://redirect.github.com/actions/setup-python/pull/938) - Revise `isGhes` logic by [@​jww3](https://redirect.github.com/jww3) in [https://github.com/actions/setup-python/pull/963](https://redirect.github.com/actions/setup-python/pull/963) - Bump pillow from 7.2 to 10.2.0 by [@​aparnajyothi-y](https://redirect.github.com/aparnajyothi-y) in [https://github.com/actions/setup-python/pull/956](https://redirect.github.com/actions/setup-python/pull/956) ##### Enhancements: - Enhance workflows and documentation updates by [@​priya-kinthali](https://redirect.github.com/priya-kinthali) in [https://github.com/actions/setup-python/pull/965](https://redirect.github.com/actions/setup-python/pull/965) - Bump default versions to latest by [@​jeffwidman](https://redirect.github.com/jeffwidman) in [https://github.com/actions/setup-python/pull/905](https://redirect.github.com/actions/setup-python/pull/905) #### New Contributors - [@​Jcambass](https://redirect.github.com/Jcambass) made their first contribution in [https://github.com/actions/setup-python/pull/941](https://redirect.github.com/actions/setup-python/pull/941) - [@​jww3](https://redirect.github.com/jww3) made their first contribution in [https://github.com/actions/setup-python/pull/963](https://redirect.github.com/actions/setup-python/pull/963) **Full Changelog**: https://github.com/actions/setup-python/compare/v5...v5.3.0 ### [`v5.2.0`](https://redirect.github.com/actions/setup-python/releases/tag/v5.2.0) [Compare Source](https://redirect.github.com/actions/setup-python/compare/v5.1.1...v5.2.0) #### What's Changed ##### Bug fixes: - Add `.zip` extension to Windows package downloads for `Expand-Archive` Compatibility by [@​priyagupta108](https://redirect.github.com/priyagupta108) in [https://github.com/actions/setup-python/pull/916](https://redirect.github.com/actions/setup-python/pull/916) This addresses compatibility issues on Windows self-hosted runners by ensuring that the filenames for Python and PyPy package downloads explicitly include the .zip extension, allowing the Expand-Archive command to function correctly. - Add arch to cache key by [@​Zxilly](https://redirect.github.com/Zxilly) in [https://github.com/actions/setup-python/pull/896](https://redirect.github.com/actions/setup-python/pull/896) This addresses issues with caching by adding the architecture (arch) to the cache key, ensuring that cache keys are accurate to prevent conflicts. Note: This change may break previous cache keys as they will no longer be compatible with the new format. ##### Documentation changes: - Fix display of emojis in contributors doc by [@​sciencewhiz](https://redirect.github.com/sciencewhiz) in [https://github.com/actions/setup-python/pull/899](https://redirect.github.com/actions/setup-python/pull/899) - Documentation update for caching poetry dependencies by [@​gowridurgad](https://redirect.github.com/gowridurgad) in [https://github.com/actions/setup-python/pull/908](https://redirect.github.com/actions/setup-python/pull/908) ##### Dependency updates: - Bump [@​iarna/toml](https://redirect.github.com/iarna/toml) version from 2.2.5 to 3.0.0 by [@​priya-kinthali](https://redirect.github.com/priya-kinthali) in [https://github.com/actions/setup-python/pull/912](https://redirect.github.com/actions/setup-python/pull/912) - Bump pyinstaller from 3.6 to 5.13.1 by [@​aparnajyothi-y](https://redirect.github.com/aparnajyothi-y) in [https://github.com/actions/setup-python/pull/923](https://redirect.github.com/actions/setup-python/pull/923) #### New Contributors - [@​sciencewhiz](https://redirect.github.com/sciencewhiz) made their first contribution in [https://github.com/actions/setup-python/pull/899](https://redirect.github.com/actions/setup-python/pull/899) - [@​priyagupta108](https://redirect.github.com/priyagupta108) made their first contribution in [https://github.com/actions/setup-python/pull/916](https://redirect.github.com/actions/setup-python/pull/916) - [@​Zxilly](https://redirect.github.com/Zxilly) made their first contribution in [https://github.com/actions/setup-python/pull/896](https://redirect.github.com/actions/setup-python/pull/896) - [@​aparnajyothi-y](https://redirect.github.com/aparnajyothi-y) made their first contribution in [https://github.com/actions/setup-python/pull/923](https://redirect.github.com/actions/setup-python/pull/923) **Full Changelog**: https://github.com/actions/setup-python/compare/v5...v5.2.0 ### [`v5.1.1`](https://redirect.github.com/actions/setup-python/releases/tag/v5.1.1) [Compare Source](https://redirect.github.com/actions/setup-python/compare/v5.1.0...v5.1.1) #### What's Changed ##### Bug fixes: - fix(ci): update all failing workflows by [@​mayeut](https://redirect.github.com/mayeut) in [https://github.com/actions/setup-python/pull/863](https://redirect.github.com/actions/setup-python/pull/863) This update ensures compatibility and optimal performance of workflows on the latest macOS version. ##### Documentation changes: - Documentation update for cache by [@​gowridurgad](https://redirect.github.com/gowridurgad) in [https://github.com/actions/setup-python/pull/873](https://redirect.github.com/actions/setup-python/pull/873) ##### Dependency updates: - Bump braces from 3.0.2 to 3.0.3 and undici from 5.28.3 to 5.28.4 by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/actions/setup-python/pull/893](https://redirect.github.com/actions/setup-python/pull/893) #### New Contributors - [@​gowridurgad](https://redirect.github.com/gowridurgad) made their first contribution in [https://github.com/actions/setup-python/pull/873](https://redirect.github.com/actions/setup-python/pull/873) **Full Changelog**: https://github.com/actions/setup-python/compare/v5...v5.1.1 ### [`v5.1.0`](https://redirect.github.com/actions/setup-python/releases/tag/v5.1.0) [Compare Source](https://redirect.github.com/actions/setup-python/compare/v5...v5.1.0) #### What's Changed - Leveraging the raw API to retrieve the version-manifest, as it does not impose a rate limit and hence facilitates unrestricted consumption without the need for a token for Github Enterprise Servers by [@​Shegox](https://redirect.github.com/Shegox) in [https://github.com/actions/setup-python/pull/766](https://redirect.github.com/actions/setup-python/pull/766). - Dependency updates by [@​dependabot](https://redirect.github.com/dependabot) and [@​HarithaVattikuti](https://redirect.github.com/HarithaVattikuti) in [https://github.com/actions/setup-python/pull/817](https://redirect.github.com/actions/setup-python/pull/817) - Documentation changes for version in README by [@​basnijholt](https://redirect.github.com/basnijholt) in [https://github.com/actions/setup-python/pull/776](https://redirect.github.com/actions/setup-python/pull/776) - Documentation changes for link in README by [@​ukd1](https://redirect.github.com/ukd1) in [https://github.com/actions/setup-python/pull/793](https://redirect.github.com/actions/setup-python/pull/793) - Documentation changes for link in Advanced Usage by [@​Jamim](https://redirect.github.com/Jamim) in [https://github.com/actions/setup-python/pull/782](https://redirect.github.com/actions/setup-python/pull/782) - Documentation changes for avoiding rate limit issues on GHES by [@​priya-kinthali](https://redirect.github.com/priya-kinthali) in [https://github.com/actions/setup-python/pull/835](https://redirect.github.com/actions/setup-python/pull/835) #### New Contributors - [@​basnijholt](https://redirect.github.com/basnijholt) made their first contribution in [https://github.com/actions/setup-python/pull/776](https://redirect.github.com/actions/setup-python/pull/776) - [@​ukd1](https://redirect.github.com/ukd1) made their first contribution in [https://github.com/actions/setup-python/pull/793](https://redirect.github.com/actions/setup-python/pull/793) - [@​Jamim](https://redirect.github.com/Jamim) made their first contribution in [https://github.com/actions/setup-python/pull/782](https://redirect.github.com/actions/setup-python/pull/782) - [@​Shegox](https://redirect.github.com/Shegox) made their first contribution in [https://github.com/actions/setup-python/pull/766](https://redirect.github.com/actions/setup-python/pull/766) - [@​priya-kinthali](https://redirect.github.com/priya-kinthali) made their first contribution in [https://github.com/actions/setup-python/pull/835](https://redirect.github.com/actions/setup-python/pull/835) **Full Changelog**: https://github.com/actions/setup-python/compare/v5.0.0...v5.1.0
--- ### Configuration 📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/astral-sh/uv). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-binaries.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml index a92db0467..0d0498453 100644 --- a/.github/workflows/build-binaries.yml +++ b/.github/workflows/build-binaries.yml @@ -704,7 +704,7 @@ jobs: steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: ${{ env.PYTHON_VERSION }} - name: "Prep README.md" From 7c90c5be02966db2d6f6cca5fe1fc12777f227b3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 16 Jun 2025 11:53:56 +0200 Subject: [PATCH 026/185] Update conda-incubator/setup-miniconda action to v3.2.0 (#14061) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [conda-incubator/setup-miniconda](https://redirect.github.com/conda-incubator/setup-miniconda) | action | minor | `v3.1.1` -> `v3.2.0` | --- > [!WARNING] > Some dependencies could not be looked up. Check the Dependency Dashboard for more information. --- ### Release Notes
conda-incubator/setup-miniconda (conda-incubator/setup-miniconda) ### [`v3.2.0`](https://redirect.github.com/conda-incubator/setup-miniconda/blob/HEAD/CHANGELOG.md#v320-2025-06-04) [Compare Source](https://redirect.github.com/conda-incubator/setup-miniconda/compare/v3.1.1...v3.2.0) ##### Fixes - Check all `.condarc` files when removing `defaults` by [@​marcoesters](https://redirect.github.com/marcoesters) in [https://github.com/conda-incubator/setup-miniconda/pull/398](https://redirect.github.com/conda-incubator/setup-miniconda/pull/398)/398 - Add version normalization for minicondaVersion in input validation by [@​jezdez](https://redirect.github.com/jezdez) [https://github.com/conda-incubator/setup-miniconda/pull/397](https://redirect.github.com/conda-incubator/setup-miniconda/pull/397)/397 - Workaround for auto_activate_base deprecation by [@​jaimergp](https://redirect.github.com/jaimergp) in [https://github.com/conda-incubator/setup-miniconda/pull/402](https://redirect.github.com/conda-incubator/setup-miniconda/pull/402)/402 ##### Tasks and Maintenance - Bump conda-incubator/setup-miniconda from 3.1.0 to 3.1.1 by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/conda-incubator/setup-miniconda/pull/391](https://redirect.github.com/conda-incubator/setup-miniconda/pull/391)/391 - Bump undici from 5.28.4 to 5.28.5 by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/conda-incubator/setup-miniconda/pull/390](https://redirect.github.com/conda-incubator/setup-miniconda/pull/390)/390 - Bump semver and [@​types/semver](https://redirect.github.com/types/semver) by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/conda-incubator/setup-miniconda/pull/399](https://redirect.github.com/conda-incubator/setup-miniconda/pull/399)/399
--- ### Configuration 📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/astral-sh/uv). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c2be5cd62..09d27b6d2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -934,7 +934,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: conda-incubator/setup-miniconda@505e6394dae86d6a5c7fbb6e3fb8938e3e863830 # v3.1.1 + - uses: conda-incubator/setup-miniconda@835234971496cad1653abb28a638a281cf32541f # v3.2.0 with: miniconda-version: latest activate-environment: uv @@ -2202,7 +2202,7 @@ jobs: steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: conda-incubator/setup-miniconda@505e6394dae86d6a5c7fbb6e3fb8938e3e863830 # v3.1.1 + - uses: conda-incubator/setup-miniconda@835234971496cad1653abb28a638a281cf32541f # v3.2.0 with: miniconda-version: "latest" activate-environment: uv From cd71ad1672f8eb1e3c8f102743f5445bba5c6267 Mon Sep 17 00:00:00 2001 From: konsti Date: Mon, 16 Jun 2025 12:14:00 +0200 Subject: [PATCH 027/185] Show retries for HTTP status code errors (#13897) Using a companion change in the middleware (https://github.com/TrueLayer/reqwest-middleware/pull/235, forked&tagged pending review), we can check and show retries for HTTP status core errors, to consistently report retries again. We fix two cases: * Show retries for status code errors for cache client requests * Show retries for status code errors for Python download requests Not handled: * Show previous retries when a distribution download fails mid-streaming * Perform retries when a distribution download fails mid-streaming * Show previous retries when a Python download fails mid-streaming * Perform retries when a Python download fails mid-streaming --- .gitignore | 3 +- Cargo.lock | 80 ++-------- Cargo.toml | 8 +- crates/uv-client/src/cached_client.rs | 148 +++++++++++++----- crates/uv-client/src/error.rs | 7 + crates/uv-client/src/flat_index.rs | 2 +- crates/uv-client/src/registry_client.rs | 31 ++-- .../src/distribution_database.rs | 16 +- crates/uv-distribution/src/source/mod.rs | 12 +- crates/uv-python/src/downloads.rs | 35 ++++- crates/uv/tests/it/edit.rs | 3 +- crates/uv/tests/it/network.rs | 6 +- 12 files changed, 205 insertions(+), 146 deletions(-) diff --git a/.gitignore b/.gitignore index 07247a33c..8ccf60790 100644 --- a/.gitignore +++ b/.gitignore @@ -3,9 +3,10 @@ # Generated by Cargo # will have compiled files and executables +/vendor/ debug/ -target/ target-alpine/ +target/ # Bootstrapped Python versions /bin/ diff --git a/Cargo.lock b/Cargo.lock index 8e24c8ba9..264a17e55 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -933,7 +933,7 @@ dependencies = [ "hashbrown 0.14.5", "lock_api", "once_cell", - "parking_lot_core 0.9.10", + "parking_lot_core", ] [[package]] @@ -1916,18 +1916,6 @@ dependencies = [ "similar", ] -[[package]] -name = "instant" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", - "js-sys", - "wasm-bindgen", - "web-sys", -] - [[package]] name = "ipnet" version = "2.11.0" @@ -2114,7 +2102,7 @@ checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ "bitflags 2.9.1", "libc", - "redox_syscall 0.5.8", + "redox_syscall", ] [[package]] @@ -2496,17 +2484,6 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" -[[package]] -name = "parking_lot" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" -dependencies = [ - "instant", - "lock_api", - "parking_lot_core 0.8.6", -] - [[package]] name = "parking_lot" version = "0.12.3" @@ -2514,21 +2491,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", - "parking_lot_core 0.9.10", -] - -[[package]] -name = "parking_lot_core" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" -dependencies = [ - "cfg-if", - "instant", - "libc", - "redox_syscall 0.2.16", - "smallvec", - "winapi", + "parking_lot_core", ] [[package]] @@ -2539,7 +2502,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.8", + "redox_syscall", "smallvec", "windows-targets 0.52.6", ] @@ -2969,15 +2932,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b42e27ef78c35d3998403c1d26f3efd9e135d3e5121b0a4845cc5cc27547f4f" -[[package]] -name = "redox_syscall" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" -dependencies = [ - "bitflags 1.3.2", -] - [[package]] name = "redox_syscall" version = "0.5.8" @@ -3137,8 +3091,7 @@ dependencies = [ [[package]] name = "reqwest-middleware" version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57f17d28a6e6acfe1733fe24bcd30774d13bffa4b8a22535b4c8c98423088d4e" +source = "git+https://github.com/astral-sh/reqwest-middleware?rev=ad8b9d332d1773fde8b4cd008486de5973e0a3f8#ad8b9d332d1773fde8b4cd008486de5973e0a3f8" dependencies = [ "anyhow", "async-trait", @@ -3152,8 +3105,7 @@ dependencies = [ [[package]] name = "reqwest-retry" version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c73e4195a6bfbcb174b790d9b3407ab90646976c55de58a6515da25d851178" +source = "git+https://github.com/astral-sh/reqwest-middleware?rev=ad8b9d332d1773fde8b4cd008486de5973e0a3f8#ad8b9d332d1773fde8b4cd008486de5973e0a3f8" dependencies = [ "anyhow", "async-trait", @@ -3161,14 +3113,13 @@ dependencies = [ "getrandom 0.2.15", "http", "hyper", - "parking_lot 0.11.2", "reqwest", "reqwest-middleware", "retry-policies", "thiserror 1.0.69", "tokio", "tracing", - "wasm-timer", + "wasmtimer", ] [[package]] @@ -3916,7 +3867,7 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96374855068f47402c3121c6eed88d29cb1de8f3ab27090e273e420bdabcf050" dependencies = [ - "parking_lot 0.12.3", + "parking_lot", ] [[package]] @@ -4158,7 +4109,7 @@ dependencies = [ "bytes", "libc", "mio", - "parking_lot 0.12.3", + "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", @@ -6183,18 +6134,17 @@ dependencies = [ ] [[package]] -name = "wasm-timer" -version = "0.2.5" +name = "wasmtimer" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be0ecb0db480561e9a7642b5d3e4187c128914e58aa84330b9493e3eb68c5e7f" +checksum = "0048ad49a55b9deb3953841fa1fc5858f0efbcb7a18868c899a360269fac1b23" dependencies = [ "futures", "js-sys", - "parking_lot 0.11.2", + "parking_lot", "pin-utils", + "slab", "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", ] [[package]] @@ -6250,7 +6200,7 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6994d13118ab492c3c80c1f81928718159254c53c472bf9ce36f8dae4add02a7" dependencies = [ - "redox_syscall 0.5.8", + "redox_syscall", "wasite", "web-sys", ] diff --git a/Cargo.toml b/Cargo.toml index fa5ecad2f..479001ac8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -143,8 +143,8 @@ reflink-copy = { version = "0.1.19" } regex = { version = "1.10.6" } regex-automata = { version = "0.4.8", default-features = false, features = ["dfa-build", "dfa-search", "perf", "std", "syntax"] } reqwest = { version = "=0.12.15", default-features = false, features = ["json", "gzip", "deflate", "zstd", "stream", "rustls-tls", "rustls-tls-native-roots", "socks", "multipart", "http2", "blocking"] } -reqwest-middleware = { version = "0.4.0", features = ["multipart"] } -reqwest-retry = { version = "0.7.0" } +reqwest-middleware = { git = "https://github.com/astral-sh/reqwest-middleware", rev = "ad8b9d332d1773fde8b4cd008486de5973e0a3f8", features = ["multipart"] } +reqwest-retry = { git = "https://github.com/astral-sh/reqwest-middleware", rev = "ad8b9d332d1773fde8b4cd008486de5973e0a3f8" } rkyv = { version = "0.8.8", features = ["bytecheck"] } rmp-serde = { version = "1.3.0" } rust-netrc = { version = "0.1.2" } @@ -372,6 +372,10 @@ riscv64gc-unknown-linux-gnu = "2.31" "actions/download-artifact" = "d3f86a106a0bac45b974a628896c90dbdf5c8093" # v4.3.0 "actions/attest-build-provenance" = "c074443f1aee8d4aeeae555aebba3282517141b2" #v2.2.3 +[patch.crates-io] +reqwest-middleware = { git = "https://github.com/astral-sh/reqwest-middleware", rev = "ad8b9d332d1773fde8b4cd008486de5973e0a3f8" } +reqwest-retry = { git = "https://github.com/astral-sh/reqwest-middleware", rev = "ad8b9d332d1773fde8b4cd008486de5973e0a3f8" } + [workspace.metadata.dist.binaries] "*" = ["uv", "uvx"] # Add "uvw" binary for Windows targets diff --git a/crates/uv-client/src/cached_client.rs b/crates/uv-client/src/cached_client.rs index 5821fe9f3..d19f95ec7 100644 --- a/crates/uv-client/src/cached_client.rs +++ b/crates/uv-client/src/cached_client.rs @@ -1,4 +1,3 @@ -use std::fmt::{Debug, Display, Formatter}; use std::time::{Duration, SystemTime}; use std::{borrow::Cow, path::Path}; @@ -100,44 +99,62 @@ where } } -/// Either a cached client error or a (user specified) error from the callback +/// Dispatch type: Either a cached client error or a (user specified) error from the callback pub enum CachedClientError { - Client(Error), - Callback(CallbackError), + Client { + retries: Option, + err: Error, + }, + Callback { + retries: Option, + err: CallbackError, + }, } -impl Display for CachedClientError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { +impl CachedClientError { + /// Attach the number of retries to the error context. + /// + /// Adds to existing errors if any, in case different layers retried. + fn with_retries(self, retries: u32) -> Self { match self { - CachedClientError::Client(err) => write!(f, "{err}"), - CachedClientError::Callback(err) => write!(f, "{err}"), + CachedClientError::Client { + retries: existing_retries, + err, + } => CachedClientError::Client { + retries: Some(existing_retries.unwrap_or_default() + retries), + err, + }, + CachedClientError::Callback { + retries: existing_retries, + err, + } => CachedClientError::Callback { + retries: Some(existing_retries.unwrap_or_default() + retries), + err, + }, } } -} -impl Debug for CachedClientError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + fn retries(&self) -> Option { match self { - CachedClientError::Client(err) => write!(f, "{err:?}"), - CachedClientError::Callback(err) => write!(f, "{err:?}"), + CachedClientError::Client { retries, .. } => *retries, + CachedClientError::Callback { retries, .. } => *retries, } } -} -impl std::error::Error - for CachedClientError -{ - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + fn error(&self) -> &dyn std::error::Error { match self { - CachedClientError::Client(err) => Some(err), - CachedClientError::Callback(err) => Some(err), + CachedClientError::Client { err, .. } => err, + CachedClientError::Callback { err, .. } => err, } } } impl From for CachedClientError { fn from(error: Error) -> Self { - Self::Client(error) + Self::Client { + retries: None, + err: error, + } } } @@ -145,15 +162,35 @@ impl From for CachedClientError { fn from(error: ErrorKind) -> Self { - Self::Client(error.into()) + Self::Client { + retries: None, + err: error.into(), + } } } impl + std::error::Error + 'static> From> for Error { + /// Attach retry error context, if there were retries. fn from(error: CachedClientError) -> Self { match error { - CachedClientError::Client(error) => error, - CachedClientError::Callback(error) => error.into(), + CachedClientError::Client { + retries: Some(retries), + err, + } => ErrorKind::RequestWithRetries { + source: Box::new(err.into_kind()), + retries, + } + .into(), + CachedClientError::Client { retries: None, err } => err, + CachedClientError::Callback { + retries: Some(retries), + err, + } => ErrorKind::RequestWithRetries { + source: Box::new(err.into().into_kind()), + retries, + } + .into(), + CachedClientError::Callback { retries: None, err } => err.into(), } } } @@ -385,7 +422,7 @@ impl CachedClient { let data = response_callback(response) .boxed_local() .await - .map_err(|err| CachedClientError::Callback(err))?; + .map_err(|err| CachedClientError::Callback { retries: None, err })?; let Some(cache_policy) = cache_policy else { return Ok(data.into_target()); }; @@ -530,9 +567,21 @@ impl CachedClient { .for_host(&url) .execute(req) .await - .map_err(|err| ErrorKind::from_reqwest_middleware(url.clone(), err))? - .error_for_status() - .map_err(|err| ErrorKind::from_reqwest(url.clone(), err))?; + .map_err(|err| ErrorKind::from_reqwest_middleware(url.clone(), err))?; + + let retry_count = response + .extensions() + .get::() + .map(|retries| retries.value()); + + if let Err(status_error) = response.error_for_status_ref() { + return Err(CachedClientError::::Client { + retries: retry_count, + err: ErrorKind::from_reqwest(url, status_error).into(), + } + .into()); + } + let cache_policy = cache_policy_builder.build(&response); let cache_policy = if cache_policy.to_archived().is_storable() { Some(Box::new(cache_policy)) @@ -579,7 +628,7 @@ impl CachedClient { cache_control: CacheControl, response_callback: Callback, ) -> Result> { - let mut n_past_retries = 0; + let mut past_retries = 0; let start_time = SystemTime::now(); let retry_policy = self.uncached().retry_policy(); loop { @@ -587,11 +636,20 @@ impl CachedClient { let result = self .get_cacheable(fresh_req, cache_entry, cache_control, &response_callback) .await; + + // Check if the middleware already performed retries + let middleware_retries = match &result { + Err(err) => err.retries().unwrap_or_default(), + Ok(_) => 0, + }; + if result .as_ref() - .is_err_and(|err| is_extended_transient_error(err)) + .is_err_and(|err| is_extended_transient_error(err.error())) { - let retry_decision = retry_policy.should_retry(start_time, n_past_retries); + // If middleware already retried, consider that in our retry budget + let total_retries = past_retries + middleware_retries; + let retry_decision = retry_policy.should_retry(start_time, total_retries); if let reqwest_retry::RetryDecision::Retry { execute_after } = retry_decision { debug!( "Transient failure while handling response from {}; retrying...", @@ -601,10 +659,15 @@ impl CachedClient { .duration_since(SystemTime::now()) .unwrap_or_else(|_| Duration::default()); tokio::time::sleep(duration).await; - n_past_retries += 1; + past_retries += 1; continue; } } + + if past_retries > 0 { + return result.map_err(|err| err.with_retries(past_retries)); + } + return result; } } @@ -622,7 +685,7 @@ impl CachedClient { cache_entry: &CacheEntry, response_callback: Callback, ) -> Result> { - let mut n_past_retries = 0; + let mut past_retries = 0; let start_time = SystemTime::now(); let retry_policy = self.uncached().retry_policy(); loop { @@ -630,12 +693,20 @@ impl CachedClient { let result = self .skip_cache(fresh_req, cache_entry, &response_callback) .await; + + // Check if the middleware already performed retries + let middleware_retries = match &result { + Err(err) => err.retries().unwrap_or_default(), + _ => 0, + }; + if result .as_ref() .err() - .is_some_and(|err| is_extended_transient_error(err)) + .is_some_and(|err| is_extended_transient_error(err.error())) { - let retry_decision = retry_policy.should_retry(start_time, n_past_retries); + let total_retries = past_retries + middleware_retries; + let retry_decision = retry_policy.should_retry(start_time, total_retries); if let reqwest_retry::RetryDecision::Retry { execute_after } = retry_decision { debug!( "Transient failure while handling response from {}; retrying...", @@ -645,10 +716,15 @@ impl CachedClient { .duration_since(SystemTime::now()) .unwrap_or_else(|_| Duration::default()); tokio::time::sleep(duration).await; - n_past_retries += 1; + past_retries += 1; continue; } } + + if past_retries > 0 { + return result.map_err(|err| err.with_retries(past_retries)); + } + return result; } } diff --git a/crates/uv-client/src/error.rs b/crates/uv-client/src/error.rs index 6629171e9..368e1ad33 100644 --- a/crates/uv-client/src/error.rs +++ b/crates/uv-client/src/error.rs @@ -197,6 +197,13 @@ pub enum ErrorKind { #[error("Failed to fetch: `{0}`")] WrappedReqwestError(DisplaySafeUrl, #[source] WrappedReqwestError), + /// Add the number of failed retries to the error. + #[error("Request failed after {retries} retries")] + RequestWithRetries { + source: Box, + retries: u32, + }, + #[error("Received some unexpected JSON from {}", url)] BadJson { source: serde_json::Error, diff --git a/crates/uv-client/src/flat_index.rs b/crates/uv-client/src/flat_index.rs index 0670fbe36..91668c5c4 100644 --- a/crates/uv-client/src/flat_index.rs +++ b/crates/uv-client/src/flat_index.rs @@ -246,7 +246,7 @@ impl<'a> FlatIndexClient<'a> { .collect(); Ok(FlatIndexEntries::from_entries(files)) } - Err(CachedClientError::Client(err)) if err.is_offline() => { + Err(CachedClientError::Client { err, .. }) if err.is_offline() => { Ok(FlatIndexEntries::offline()) } Err(err) => Err(err.into()), diff --git a/crates/uv-client/src/registry_client.rs b/crates/uv-client/src/registry_client.rs index 7dbdf7e49..6271b7d20 100644 --- a/crates/uv-client/src/registry_client.rs +++ b/crates/uv-client/src/registry_client.rs @@ -41,10 +41,7 @@ use crate::flat_index::FlatIndexEntry; use crate::html::SimpleHtml; use crate::remote_metadata::wheel_metadata_from_remote_zip; use crate::rkyvutil::OwnedArchive; -use crate::{ - BaseClient, CachedClient, CachedClientError, Error, ErrorKind, FlatIndexClient, - FlatIndexEntries, -}; +use crate::{BaseClient, CachedClient, Error, ErrorKind, FlatIndexClient, FlatIndexEntries}; /// A builder for an [`RegistryClient`]. #[derive(Debug, Clone)] @@ -607,18 +604,16 @@ impl RegistryClient { .boxed_local() .instrument(info_span!("parse_simple_api", package = %package_name)) }; - self.cached_client() + let simple = self + .cached_client() .get_cacheable_with_retry( simple_request, cache_entry, cache_control, parse_simple_response, ) - .await - .map_err(|err| match err { - CachedClientError::Client(err) => err, - CachedClientError::Callback(err) => err, - }) + .await?; + Ok(simple) } /// Fetch the [`SimpleMetadata`] from a local file, using a PEP 503-compatible directory @@ -900,15 +895,13 @@ impl RegistryClient { .map_err(|err| ErrorKind::AsyncHttpRangeReader(url.clone(), err))?; trace!("Getting metadata for {filename} by range request"); let text = wheel_metadata_from_remote_zip(filename, url, &mut reader).await?; - let metadata = - ResolutionMetadata::parse_metadata(text.as_bytes()).map_err(|err| { - Error::from(ErrorKind::MetadataParseError( - filename.clone(), - url.to_string(), - Box::new(err), - )) - })?; - Ok::>(metadata) + ResolutionMetadata::parse_metadata(text.as_bytes()).map_err(|err| { + Error::from(ErrorKind::MetadataParseError( + filename.clone(), + url.to_string(), + Box::new(err), + )) + }) } .boxed_local() .instrument(info_span!("read_metadata_range_request", wheel = %filename)) diff --git a/crates/uv-distribution/src/distribution_database.rs b/crates/uv-distribution/src/distribution_database.rs index 0ecea36e6..dcb0a17e3 100644 --- a/crates/uv-distribution/src/distribution_database.rs +++ b/crates/uv-distribution/src/distribution_database.rs @@ -644,8 +644,8 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { }) .await .map_err(|err| match err { - CachedClientError::Callback(err) => err, - CachedClientError::Client(err) => Error::Client(err), + CachedClientError::Callback { err, .. } => err, + CachedClientError::Client { err, .. } => Error::Client(err), })?; // If the archive is missing the required hashes, or has since been removed, force a refresh. @@ -663,8 +663,8 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { .skip_cache_with_retry(self.request(url)?, &http_entry, download) .await .map_err(|err| match err { - CachedClientError::Callback(err) => err, - CachedClientError::Client(err) => Error::Client(err), + CachedClientError::Callback { err, .. } => err, + CachedClientError::Client { err, .. } => Error::Client(err), }) }) .await? @@ -811,8 +811,8 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { }) .await .map_err(|err| match err { - CachedClientError::Callback(err) => err, - CachedClientError::Client(err) => Error::Client(err), + CachedClientError::Callback { err, .. } => err, + CachedClientError::Client { err, .. } => Error::Client(err), })?; // If the archive is missing the required hashes, or has since been removed, force a refresh. @@ -830,8 +830,8 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { .skip_cache_with_retry(self.request(url)?, &http_entry, download) .await .map_err(|err| match err { - CachedClientError::Callback(err) => err, - CachedClientError::Client(err) => Error::Client(err), + CachedClientError::Callback { err, .. } => err, + CachedClientError::Client { err, .. } => Error::Client(err), }) }) .await? diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs index 7a92d700f..2cf148f2b 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -728,8 +728,8 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { }) .await .map_err(|err| match err { - CachedClientError::Callback(err) => err, - CachedClientError::Client(err) => Error::Client(err), + CachedClientError::Callback { err, .. } => err, + CachedClientError::Client { err, .. } => Error::Client(err), })?; // If the archive is missing the required hashes, force a refresh. @@ -747,8 +747,8 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { ) .await .map_err(|err| match err { - CachedClientError::Callback(err) => err, - CachedClientError::Client(err) => Error::Client(err), + CachedClientError::Callback { err, .. } => err, + CachedClientError::Client { err, .. } => Error::Client(err), }) }) .await @@ -2084,8 +2084,8 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { ) .await .map_err(|err| match err { - CachedClientError::Callback(err) => err, - CachedClientError::Client(err) => Error::Client(err), + CachedClientError::Callback { err, .. } => err, + CachedClientError::Client { err, .. } => Error::Client(err), }) }) .await diff --git a/crates/uv-python/src/downloads.rs b/crates/uv-python/src/downloads.rs index 0b7517960..fd3f6c417 100644 --- a/crates/uv-python/src/downloads.rs +++ b/crates/uv-python/src/downloads.rs @@ -53,6 +53,12 @@ pub enum Error { TooManyParts(String), #[error("Failed to download {0}")] NetworkError(DisplaySafeUrl, #[source] WrappedReqwestError), + #[error("Request failed after {retries} retries")] + NetworkErrorWithRetries { + #[source] + err: Box, + retries: u32, + }, #[error("Failed to download {0}")] NetworkMiddlewareError(DisplaySafeUrl, #[source] anyhow::Error), #[error("Failed to extract archive: {0}")] @@ -1143,8 +1149,20 @@ fn parse_json_downloads( } impl Error { - pub(crate) fn from_reqwest(url: DisplaySafeUrl, err: reqwest::Error) -> Self { - Self::NetworkError(url, WrappedReqwestError::from(err)) + pub(crate) fn from_reqwest( + url: DisplaySafeUrl, + err: reqwest::Error, + retries: Option, + ) -> Self { + let err = Self::NetworkError(url, WrappedReqwestError::from(err)); + if let Some(retries) = retries { + Self::NetworkErrorWithRetries { + err: Box::new(err), + retries, + } + } else { + err + } } pub(crate) fn from_reqwest_middleware( @@ -1260,10 +1278,15 @@ async fn read_url( .await .map_err(|err| Error::from_reqwest_middleware(url.clone(), err))?; - // Ensure the request was successful. - response - .error_for_status_ref() - .map_err(|err| Error::from_reqwest(url, err))?; + let retry_count = response + .extensions() + .get::() + .map(|retries| retries.value()); + + // Check the status code. + let response = response + .error_for_status() + .map_err(|err| Error::from_reqwest(url, err, retry_count))?; let size = response.content_length(); let stream = response diff --git a/crates/uv/tests/it/edit.rs b/crates/uv/tests/it/edit.rs index 10c812473..a66cd2ed5 100644 --- a/crates/uv/tests/it/edit.rs +++ b/crates/uv/tests/it/edit.rs @@ -11477,7 +11477,8 @@ async fn add_unexpected_error_code() -> Result<()> { ----- stdout ----- ----- stderr ----- - error: Failed to fetch: `http://[LOCALHOST]/anyio/` + error: Request failed after 3 retries + Caused by: Failed to fetch: `http://[LOCALHOST]/anyio/` Caused by: HTTP status server error (503 Service Unavailable) for url (http://[LOCALHOST]/anyio/) " ); diff --git a/crates/uv/tests/it/network.rs b/crates/uv/tests/it/network.rs index 8edbc63a2..1a5805970 100644 --- a/crates/uv/tests/it/network.rs +++ b/crates/uv/tests/it/network.rs @@ -54,7 +54,8 @@ async fn simple_http_500() { ----- stdout ----- ----- stderr ----- - error: Failed to fetch: `[SERVER]/tqdm/` + error: Request failed after 3 retries + Caused by: Failed to fetch: `[SERVER]/tqdm/` Caused by: HTTP status server error (500 Internal Server Error) for url ([SERVER]/tqdm/) "); } @@ -105,6 +106,7 @@ async fn find_links_http_500() { ----- stderr ----- error: Failed to read `--find-links` URL: [SERVER]/ + Caused by: Request failed after 3 retries Caused by: Failed to fetch: `[SERVER]/` Caused by: HTTP status server error (500 Internal Server Error) for url ([SERVER]/) "); @@ -159,6 +161,7 @@ async fn direct_url_http_500() { ----- stderr ----- × Failed to download `tqdm @ [SERVER]/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl` + ├─▶ Request failed after 3 retries ├─▶ Failed to fetch: `[SERVER]/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl` ╰─▶ HTTP status server error (500 Internal Server Error) for url ([SERVER]/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl) "); @@ -243,6 +246,7 @@ async fn python_install_http_500() { ----- stderr ----- error: Failed to install cpython-3.10.0-macos-aarch64-none + Caused by: Request failed after 3 retries Caused by: Failed to download [SERVER]/astral-sh/python-build-standalone/releases/download/20211017/cpython-3.10.0-aarch64-apple-darwin-pgo%2Blto-20211017T1616.tar.zst Caused by: HTTP status server error (500 Internal Server Error) for url ([SERVER]/astral-sh/python-build-standalone/releases/download/20211017/cpython-3.10.0-aarch64-apple-darwin-pgo%2Blto-20211017T1616.tar.zst) "); From 5c1ebf902bad2ed8058b57deaca9645de1286eca Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Mon, 16 Jun 2025 10:05:56 -0400 Subject: [PATCH 028/185] Add `rustfmt.toml` (#14072) We've gotten away without this file for a while. In particular, we explicitly use its default settings. However, this is occasionally problematic in certain contexts where `rustfmt` is invoked directly. Or in contexts where the Rust Edition is otherwise not specified. At least, this happens when using the Rust vim plugin. When an edition isn't explicitly specified, it defaults back to the 2015 edition. I think that there aren't a lot of rustfmt changes, and so we've been able to get away with this for a while. But it looks like something in the 2024 edition changes how imports are ordered. So to make it explicit that we want to use the 2024 edition of rustfmt, we opt into it. This is analogous to a change made to the Ruff repository somewhat recently: https://github.com/astral-sh/ruff/pull/18197 --- rustfmt.toml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 rustfmt.toml diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 000000000..f3e454b61 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,2 @@ +edition = "2024" +style_edition = "2024" From 423cfaabf5322f2797ca5b0239de56de7f36a7a2 Mon Sep 17 00:00:00 2001 From: konsti Date: Mon, 16 Jun 2025 19:10:21 +0200 Subject: [PATCH 029/185] Show backtraces for CI crashes (#14081) I only just realized that we can get backtraces for crashes with `RUST_BACKTRACE: 1`, even non-panics (https://github.com/astral-sh/uv/pull/14079). This is much better than trying to analyze crash dumps. --- .github/workflows/ci.yml | 55 ++-------------------------------------- 1 file changed, 2 insertions(+), 53 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 09d27b6d2..8a1f78c9e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,8 +14,9 @@ env: CARGO_INCREMENTAL: 0 CARGO_NET_RETRY: 10 CARGO_TERM_COLOR: always - RUSTUP_MAX_RETRIES: 10 PYTHON_VERSION: "3.12" + RUSTUP_MAX_RETRIES: 10 + RUST_BACKTRACE: 1 jobs: determine_changes: @@ -295,23 +296,7 @@ jobs: with: tool: cargo-nextest - # Get crash dumps to debug the `exit_code: -1073741819` failures - - name: Configure crash dumps - if: runner.os == 'Windows' - shell: powershell - run: | - $dumps = "$env:GITHUB_WORKSPACE\dumps" - New-Item -Path $dumps -ItemType Directory -Force - - # https://github.com/microsoft/terminal/wiki/Troubleshooting-Tips#capture-automatically - $reg = "HKLM:\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps" - New-Item -Path $reg -Force | Out-Null - Set-ItemProperty -Path $reg -Name "DumpFolder" -Value $dumps - Set-ItemProperty -Path $reg -Name "DumpType" -Value 2 - - name: "Cargo test" - id: test - continue-on-error: true working-directory: ${{ env.UV_WORKSPACE }} env: # Avoid permission errors during concurrent tests @@ -325,42 +310,6 @@ jobs: --workspace \ --status-level skip --failure-output immediate-final --no-fail-fast -j 20 --final-status-level slow - # Get crash dumps to debug the `exit_code: -1073741819` failures (contd.) - - name: Analyze crashes - if: steps.test.outcome == 'failure' - shell: powershell - run: | - $dumps = Get-ChildItem "$env:GITHUB_WORKSPACE\dumps\*.dmp" -ErrorAction SilentlyContinue - if (!$dumps) { exit 0 } - - Write-Host "Found $($dumps.Count) crash dump(s)" - - # Download cdb if needed - $cdb = "C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\cdb.exe" - if (!(Test-Path $cdb)) { - # https://github.com/microsoft/react-native-windows/blob/f1570a5ef1c4fc1e78d0a0ad5af848ab91a4061c/vnext/Scripts/Analyze-Crash.ps1#L44-L56 - Invoke-WebRequest "https://go.microsoft.com/fwlink/?linkid=2173743" -OutFile "$env:TEMP\sdk.exe" - Start-Process "$env:TEMP\sdk.exe" -ArgumentList "/features OptionId.WindowsDesktopDebuggers /quiet" -Wait - } - - # Analyze each dump - foreach ($dump in $dumps) { - Write-Host "`n=== $($dump.Name) ===" - & $cdb -z $dump -c "!analyze -v; .ecxr; k; q" 2>&1 | Select-String -Pattern "(ExceptionCode:|SYMBOL_NAME:|IMAGE_NAME:|STACK_TEXT:)" -Context 0,2 - } - - - name: Upload crash dumps - if: steps.test.outcome == 'failure' - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 - with: - name: crash-dumps-${{ github.run_number }} - path: dumps/*.dmp - if-no-files-found: ignore - - - name: Fail if tests failed - if: steps.test.outcome == 'failure' - run: exit 1 - # Separate jobs for the nightly crate windows-trampoline-check: timeout-minutes: 15 From d73d3e8b536065e63a534fa1126d96e792003b05 Mon Sep 17 00:00:00 2001 From: Kyle Galbraith Date: Mon, 16 Jun 2025 20:32:35 +0200 Subject: [PATCH 030/185] Refactor Docker image builds to use Depot (#13459) ## Summary Simplify the Docker image build process to leverage Depot container builders for faster layer caching and native multi-platform image builds. The combo of the two removes the need to save cache to registries and do complex merge operations across GHA runners to get a multi-platform image. ## Test Plan UV team will need to add a trust relationship in Depot so that the container builds can authenticate and run. This can be done following these docs: https://depot.dev/docs/cli/authentication#adding-a-trust-relationship-for-github-actions. Once that is done, this should just work as before, but without all of the extra work around manifests. We should double that all of the tagging still makes sense for you all, as some bits of that were unclear. Additional context in this draft PR: https://github.com/astral-sh/uv/pull/9156 --------- Co-authored-by: Zanie Blue --- .github/workflows/build-docker.yml | 234 +++++++---------------------- 1 file changed, 57 insertions(+), 177 deletions(-) diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml index 1488774b0..ce12d3c5d 100644 --- a/.github/workflows/build-docker.yml +++ b/.github/workflows/build-docker.yml @@ -1,11 +1,19 @@ -# Build and publish a Docker image. +# Build and publish Docker images. # -# Assumed to run as a subworkflow of .github/workflows/release.yml; specifically, as a local -# artifacts job within `cargo-dist`. +# Uses Depot for multi-platform builds. Includes both a `uv` base image, which +# is just the binary in a scratch image, and a set of extra, common images with +# the uv binary installed. # -# TODO(charlie): Ideally, the publish step would happen as a publish job within `cargo-dist`, but -# sharing the built image as an artifact between jobs is challenging. -name: "Build Docker image" +# Images are built on all runs. +# +# On release, assumed to run as a subworkflow of .github/workflows/release.yml; +# specifically, as a local artifacts job within `cargo-dist`. In this case, +# images are published based on the `plan`. +# +# TODO(charlie): Ideally, the publish step would happen as a publish job within +# `cargo-dist`, but sharing the built image as an artifact between jobs is +# challenging. +name: "Docker images" on: workflow_call: @@ -32,18 +40,19 @@ env: UV_BASE_IMG: ghcr.io/${{ github.repository_owner }}/uv jobs: - docker-build: + docker-publish-base: if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }} - name: Build Docker image (ghcr.io/astral-sh/uv) for ${{ matrix.platform }} + name: uv runs-on: ubuntu-latest + permissions: + contents: read + id-token: write # for Depot OIDC + packages: write # for GHCR environment: name: release - strategy: - fail-fast: false - matrix: - platform: - - linux/amd64 - - linux/arm64 + outputs: + image-tags: ${{ steps.meta.outputs.tags }} + image-digest: ${{ steps.build.outputs.digest }} steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: @@ -57,14 +66,14 @@ jobs: username: astralshbot password: ${{ secrets.DOCKERHUB_TOKEN_RO }} - - uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 - - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} + - uses: depot/setup-action@b0b1ea4f69e92ebf5dea3f8713a1b0c37b2126a5 + - name: Check tag consistency if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }} run: | @@ -87,96 +96,33 @@ jobs: tags: | type=raw,value=dry-run,enable=${{ inputs.plan == '' || fromJson(inputs.plan).announcement_tag_is_implicit }} type=pep440,pattern={{ version }},value=${{ inputs.plan != '' && fromJson(inputs.plan).announcement_tag || 'dry-run' }},enable=${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }} + type=pep440,pattern={{ major }}.{{ minor }},value=${{ inputs.plan != '' && fromJson(inputs.plan).announcement_tag || 'dry-run' }},enable=${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }} - - name: Normalize Platform Pair (replace / with -) - run: | - platform=${{ matrix.platform }} - echo "PLATFORM_TUPLE=${platform//\//-}" >> $GITHUB_ENV - - # Adapted from https://docs.docker.com/build/ci/github-actions/multi-platform/ - name: Build and push by digest id: build - uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 + uses: depot/build-push-action@636daae76684e38c301daa0c5eca1c095b24e780 # v1.14.0 with: + project: 7hd4vdzmw5 # astral-sh/uv context: . - platforms: ${{ matrix.platform }} - cache-from: type=gha,scope=uv-${{ env.PLATFORM_TUPLE }} - cache-to: type=gha,mode=min,scope=uv-${{ env.PLATFORM_TUPLE }} + platforms: linux/amd64,linux/arm64 + push: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }} + tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - outputs: type=image,name=${{ env.UV_BASE_IMG }},push-by-digest=true,name-canonical=true,push=${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }} - - name: Export digests - run: | - mkdir -p /tmp/digests - digest="${{ steps.build.outputs.digest }}" - touch "/tmp/digests/${digest#sha256:}" - - - name: Upload digests - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + - name: Generate artifact attestation for base image + if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }} + uses: actions/attest-build-provenance@c074443f1aee8d4aeeae555aebba3282517141b2 # v2.2.3 with: - name: digests-${{ env.PLATFORM_TUPLE }} - path: /tmp/digests/* - if-no-files-found: error - retention-days: 1 - - docker-publish: - name: Publish Docker image (ghcr.io/astral-sh/uv) - runs-on: ubuntu-latest - environment: - name: release - needs: - - docker-build - if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }} - steps: - # Login to DockerHub first, to avoid rate-limiting - - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 - with: - username: astralshbot - password: ${{ secrets.DOCKERHUB_TOKEN_RO }} - - - name: Download digests - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 - with: - path: /tmp/digests - pattern: digests-* - merge-multiple: true - - - uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 - - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0 - with: - images: ${{ env.UV_BASE_IMG }} - # Order is on purpose such that the label org.opencontainers.image.version has the first pattern with the full version - tags: | - type=pep440,pattern={{ version }},value=${{ fromJson(inputs.plan).announcement_tag }} - type=pep440,pattern={{ major }}.{{ minor }},value=${{ fromJson(inputs.plan).announcement_tag }} - - - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - # Adapted from https://docs.docker.com/build/ci/github-actions/multi-platform/ - - name: Create manifest list and push - working-directory: /tmp/digests - # The jq command expands the docker/metadata json "tags" array entry to `-t tag1 -t tag2 ...` for each tag in the array - # The printf will expand the base image with the `@sha256: ...` for each sha256 in the directory - # The final command becomes `docker buildx imagetools create -t tag1 -t tag2 ... @sha256: @sha256: ...` - run: | - docker buildx imagetools create \ - $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ - $(printf '${{ env.UV_BASE_IMG }}@sha256:%s ' *) + subject-name: ${{ env.UV_BASE_IMG }} + subject-digest: ${{ steps.build.outputs.digest }} docker-publish-extra: - name: Publish additional Docker image based on ${{ matrix.image-mapping }} + name: ${{ matrix.image-mapping }} runs-on: ubuntu-latest environment: name: release needs: - - docker-publish + - docker-publish-base if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }} permissions: packages: write @@ -215,18 +161,19 @@ jobs: steps: # Login to DockerHub first, to avoid rate-limiting - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 + if: ${{ github.event.pull_request.head.repo.full_name == 'astral-sh/uv' }} with: username: astralshbot password: ${{ secrets.DOCKERHUB_TOKEN_RO }} - - uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 - - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} + - uses: depot/setup-action@b0b1ea4f69e92ebf5dea3f8713a1b0c37b2126a5 + - name: Generate Dynamic Dockerfile Tags shell: bash run: | @@ -257,9 +204,6 @@ jobs: # Remove the trailing newline from the pattern list TAG_PATTERNS="${TAG_PATTERNS%\\n}" - # Export image cache name - echo "IMAGE_REF=${BASE_IMAGE//:/-}" >> $GITHUB_ENV - # Export tag patterns using the multiline env var syntax { echo "TAG_PATTERNS<@sha256: ...` for each sha256 in the directory - # The final command becomes `docker buildx imagetools create -t tag1 -t tag2 ... @sha256: @sha256: ...` + - name: Push tags + env: + IMAGE: ${{ env.UV_BASE_IMG }} + DIGEST: ${{ needs.docker-publish-base.outputs.image-digest }} + TAGS: ${{ needs.docker-publish-base.outputs.image-tags }} run: | - readarray -t lines <<< "$DOCKER_METADATA_OUTPUT_ANNOTATIONS"; annotations=(); for line in "${lines[@]}"; do annotations+=(--annotation "$line"); done - docker buildx imagetools create \ - "${annotations[@]}" \ - $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ - $(printf '${{ env.UV_BASE_IMG }}@sha256:%s ' *) - - - name: Share manifest digest - id: manifest-digest - # To sign the manifest, we need it's digest. Unfortunately "docker - # buildx imagetools create" does not (yet) have a clean way of sharing - # the digest of the manifest it creates (see docker/buildx#2407), so - # we use a separate command to retrieve it. - # imagetools inspect [TAG] --format '{{json .Manifest}}' gives us - # the machine readable JSON description of the manifest, and the - # jq command extracts the digest from this. The digest is then - # sent to the Github step output file for sharing with other steps. - run: | - digest="$( - docker buildx imagetools inspect \ - "${UV_BASE_IMG}:${DOCKER_METADATA_OUTPUT_VERSION}" \ - --format '{{json .Manifest}}' \ - | jq -r '.digest' - )" - echo "digest=${digest}" >> "$GITHUB_OUTPUT" - - - name: Generate artifact attestation - uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0 - with: - subject-name: ${{ env.UV_BASE_IMG }} - subject-digest: ${{ steps.manifest-digest.outputs.digest }} - # push-to-registry is explicitly not enabled to maintain full control over the top image + docker pull "${IMAGE}@${DIGEST}" + for tag in $TAGS; do + docker tag "${IMAGE}@${DIGEST}" "${tag}" + docker push "${tag}" + done From cf67d9c633c1545ae5f46a1020182f4cef62ae76 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Mon, 16 Jun 2025 15:01:17 -0500 Subject: [PATCH 031/185] Clear `XDG_DATA_HOME` during test runs (#14075) --- crates/uv/tests/it/common/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/uv/tests/it/common/mod.rs b/crates/uv/tests/it/common/mod.rs index 23845a8a8..66eb21729 100644 --- a/crates/uv/tests/it/common/mod.rs +++ b/crates/uv/tests/it/common/mod.rs @@ -747,6 +747,7 @@ impl TestContext { .env_remove(EnvVars::UV_CACHE_DIR) .env_remove(EnvVars::UV_TOOL_BIN_DIR) .env_remove(EnvVars::XDG_CONFIG_HOME) + .env_remove(EnvVars::XDG_DATA_HOME) .current_dir(self.temp_dir.path()); for (key, value) in &self.extra_env { From e02cd74e64bd918dc8adea96e15a96a1c3c08d08 Mon Sep 17 00:00:00 2001 From: John Mumm Date: Tue, 17 Jun 2025 06:18:54 -0400 Subject: [PATCH 032/185] Turn off `clippy::struct_excessive_bools` rule (#14102) We always ignore the `clippy::struct_excessive_bools` rule and formerly annotated this at the function level. This PR specifies the allow in `workspace.lints.clippy` in `Cargo.toml`. --- Cargo.toml | 1 + crates/uv-cli/src/compat.rs | 6 --- crates/uv-cli/src/lib.rs | 48 -------------------- crates/uv-client/src/httpcache/control.rs | 1 - crates/uv-python/src/virtualenv.rs | 1 - crates/uv-resolver/src/resolution/display.rs | 1 - crates/uv-resolver/src/version_map.rs | 1 - crates/uv/src/settings.rs | 32 ------------- 8 files changed, 1 insertion(+), 90 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 479001ac8..c6d5729e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -214,6 +214,7 @@ missing_panics_doc = "allow" module_name_repetitions = "allow" must_use_candidate = "allow" similar_names = "allow" +struct_excessive_bools = "allow" too_many_arguments = "allow" too_many_lines = "allow" used_underscore_binding = "allow" diff --git a/crates/uv-cli/src/compat.rs b/crates/uv-cli/src/compat.rs index 50f4c173d..d29afa760 100644 --- a/crates/uv-cli/src/compat.rs +++ b/crates/uv-cli/src/compat.rs @@ -13,7 +13,6 @@ pub trait CompatArgs { /// For example, users often pass `--allow-unsafe`, which is unnecessary with uv. But it's a /// nice user experience to warn, rather than fail, when users pass `--allow-unsafe`. #[derive(Args)] -#[allow(clippy::struct_excessive_bools)] pub struct PipCompileCompatArgs { #[clap(long, hide = true)] allow_unsafe: bool, @@ -159,7 +158,6 @@ impl CompatArgs for PipCompileCompatArgs { /// /// These represent a subset of the `pip list` interface that uv supports by default. #[derive(Args)] -#[allow(clippy::struct_excessive_bools)] pub struct PipListCompatArgs { #[clap(long, hide = true)] disable_pip_version_check: bool, @@ -184,7 +182,6 @@ impl CompatArgs for PipListCompatArgs { /// /// These represent a subset of the `pip-sync` interface that uv supports by default. #[derive(Args)] -#[allow(clippy::struct_excessive_bools)] pub struct PipSyncCompatArgs { #[clap(short, long, hide = true)] ask: bool, @@ -268,7 +265,6 @@ enum Resolver { /// /// These represent a subset of the `virtualenv` interface that uv supports by default. #[derive(Args)] -#[allow(clippy::struct_excessive_bools)] pub struct VenvCompatArgs { #[clap(long, hide = true)] clear: bool, @@ -327,7 +323,6 @@ impl CompatArgs for VenvCompatArgs { /// /// These represent a subset of the `pip install` interface that uv supports by default. #[derive(Args)] -#[allow(clippy::struct_excessive_bools)] pub struct PipInstallCompatArgs { #[clap(long, hide = true)] disable_pip_version_check: bool, @@ -361,7 +356,6 @@ impl CompatArgs for PipInstallCompatArgs { /// /// These represent a subset of the `pip` interface that exists on all commands. #[derive(Args)] -#[allow(clippy::struct_excessive_bools)] pub struct PipGlobalCompatArgs { #[clap(long, hide = true)] disable_pip_version_check: bool, diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 0b96875e5..bd06f9a82 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -85,7 +85,6 @@ const STYLES: Styles = Styles::styled() disable_version_flag = true )] #[command(styles=STYLES)] -#[allow(clippy::struct_excessive_bools)] pub struct Cli { #[command(subcommand)] pub command: Box, @@ -133,7 +132,6 @@ pub struct TopLevelArgs { #[derive(Parser, Debug, Clone)] #[command(next_help_heading = "Global options", next_display_order = 1000)] -#[allow(clippy::struct_excessive_bools)] pub struct GlobalArgs { #[arg( global = true, @@ -526,7 +524,6 @@ pub struct HelpArgs { #[derive(Args)] #[command(group = clap::ArgGroup::new("operation"))] -#[allow(clippy::struct_excessive_bools)] pub struct VersionArgs { /// Set the project version to this value /// @@ -657,7 +654,6 @@ pub struct SelfUpdateArgs { } #[derive(Args)] -#[allow(clippy::struct_excessive_bools)] pub struct CacheNamespace { #[command(subcommand)] pub command: CacheCommand, @@ -687,14 +683,12 @@ pub enum CacheCommand { } #[derive(Args, Debug)] -#[allow(clippy::struct_excessive_bools)] pub struct CleanArgs { /// The packages to remove from the cache. pub package: Vec, } #[derive(Args, Debug)] -#[allow(clippy::struct_excessive_bools)] pub struct PruneArgs { /// Optimize the cache for persistence in a continuous integration environment, like GitHub /// Actions. @@ -714,7 +708,6 @@ pub struct PruneArgs { } #[derive(Args)] -#[allow(clippy::struct_excessive_bools)] pub struct PipNamespace { #[command(subcommand)] pub command: PipCommand, @@ -1095,7 +1088,6 @@ fn parse_maybe_string(input: &str) -> Result, String> { } #[derive(Args)] -#[allow(clippy::struct_excessive_bools)] #[command(group = clap::ArgGroup::new("sources").required(true).multiple(true))] pub struct PipCompileArgs { /// Include all packages listed in the given `requirements.in` files. @@ -1443,7 +1435,6 @@ pub struct PipCompileArgs { } #[derive(Args)] -#[allow(clippy::struct_excessive_bools)] pub struct PipSyncArgs { /// Include all packages listed in the given `requirements.txt` files. /// @@ -1700,7 +1691,6 @@ pub struct PipSyncArgs { #[derive(Args)] #[command(group = clap::ArgGroup::new("sources").required(true).multiple(true))] -#[allow(clippy::struct_excessive_bools)] pub struct PipInstallArgs { /// Install all listed packages. /// @@ -2015,7 +2005,6 @@ pub struct PipInstallArgs { #[derive(Args)] #[command(group = clap::ArgGroup::new("sources").required(true).multiple(true))] -#[allow(clippy::struct_excessive_bools)] pub struct PipUninstallArgs { /// Uninstall all listed packages. #[arg(group = "sources")] @@ -2104,7 +2093,6 @@ pub struct PipUninstallArgs { } #[derive(Args)] -#[allow(clippy::struct_excessive_bools)] pub struct PipFreezeArgs { /// Exclude any editable packages from output. #[arg(long)] @@ -2159,7 +2147,6 @@ pub struct PipFreezeArgs { } #[derive(Args)] -#[allow(clippy::struct_excessive_bools)] pub struct PipListArgs { /// Only include editable projects. #[arg(short, long)] @@ -2235,7 +2222,6 @@ pub struct PipListArgs { } #[derive(Args)] -#[allow(clippy::struct_excessive_bools)] pub struct PipCheckArgs { /// The Python interpreter for which packages should be checked. /// @@ -2271,7 +2257,6 @@ pub struct PipCheckArgs { } #[derive(Args)] -#[allow(clippy::struct_excessive_bools)] pub struct PipShowArgs { /// The package(s) to display. pub package: Vec, @@ -2325,7 +2310,6 @@ pub struct PipShowArgs { } #[derive(Args)] -#[allow(clippy::struct_excessive_bools)] pub struct PipTreeArgs { /// Show the version constraint(s) imposed on each package. #[arg(long)] @@ -2382,7 +2366,6 @@ pub struct PipTreeArgs { } #[derive(Args)] -#[allow(clippy::struct_excessive_bools)] pub struct BuildArgs { /// The directory from which distributions should be built, or a source /// distribution archive to build into a wheel. @@ -2529,7 +2512,6 @@ pub struct BuildArgs { } #[derive(Args)] -#[allow(clippy::struct_excessive_bools)] pub struct VenvArgs { /// The Python interpreter to use for the virtual environment. /// @@ -2725,7 +2707,6 @@ pub enum AuthorFrom { } #[derive(Args)] -#[allow(clippy::struct_excessive_bools)] pub struct InitArgs { /// The path to use for the project/script. /// @@ -2883,7 +2864,6 @@ pub struct InitArgs { } #[derive(Args)] -#[allow(clippy::struct_excessive_bools)] pub struct RunArgs { /// Include optional dependencies from the specified extra name. /// @@ -3170,7 +3150,6 @@ pub struct RunArgs { } #[derive(Args)] -#[allow(clippy::struct_excessive_bools)] pub struct SyncArgs { /// Include optional dependencies from the specified extra name. /// @@ -3427,7 +3406,6 @@ pub struct SyncArgs { } #[derive(Args)] -#[allow(clippy::struct_excessive_bools)] pub struct LockArgs { /// Check if the lockfile is up-to-date. /// @@ -3489,7 +3467,6 @@ pub struct LockArgs { #[derive(Args)] #[command(group = clap::ArgGroup::new("sources").required(true).multiple(true))] -#[allow(clippy::struct_excessive_bools)] pub struct AddArgs { /// The packages to add, as PEP 508 requirements (e.g., `ruff==0.5.0`). #[arg(group = "sources")] @@ -3674,7 +3651,6 @@ pub struct AddArgs { } #[derive(Args)] -#[allow(clippy::struct_excessive_bools)] pub struct RemoveArgs { /// The names of the dependencies to remove (e.g., `ruff`). #[arg(required = true)] @@ -3769,7 +3745,6 @@ pub struct RemoveArgs { } #[derive(Args)] -#[allow(clippy::struct_excessive_bools)] pub struct TreeArgs { /// Show a platform-independent dependency tree. /// @@ -3909,7 +3884,6 @@ pub struct TreeArgs { } #[derive(Args)] -#[allow(clippy::struct_excessive_bools)] pub struct ExportArgs { /// The format to which `uv.lock` should be exported. /// @@ -4124,7 +4098,6 @@ pub struct ExportArgs { } #[derive(Args)] -#[allow(clippy::struct_excessive_bools)] pub struct ToolNamespace { #[command(subcommand)] pub command: ToolCommand, @@ -4217,7 +4190,6 @@ pub enum ToolCommand { } #[derive(Args)] -#[allow(clippy::struct_excessive_bools)] pub struct ToolRunArgs { /// The command to run. /// @@ -4336,7 +4308,6 @@ pub struct UvxArgs { } #[derive(Args)] -#[allow(clippy::struct_excessive_bools)] pub struct ToolInstallArgs { /// The package to install commands from. pub package: String, @@ -4425,7 +4396,6 @@ pub struct ToolInstallArgs { } #[derive(Args)] -#[allow(clippy::struct_excessive_bools)] pub struct ToolListArgs { /// Whether to display the path to each tool environment and installed executable. #[arg(long)] @@ -4452,7 +4422,6 @@ pub struct ToolListArgs { } #[derive(Args)] -#[allow(clippy::struct_excessive_bools)] pub struct ToolDirArgs { /// Show the directory into which `uv tool` will install executables. /// @@ -4471,7 +4440,6 @@ pub struct ToolDirArgs { } #[derive(Args)] -#[allow(clippy::struct_excessive_bools)] pub struct ToolUninstallArgs { /// The name of the tool to uninstall. #[arg(required = true)] @@ -4483,7 +4451,6 @@ pub struct ToolUninstallArgs { } #[derive(Args)] -#[allow(clippy::struct_excessive_bools)] pub struct ToolUpgradeArgs { /// The name of the tool to upgrade, along with an optional version specifier. #[arg(required = true)] @@ -4713,7 +4680,6 @@ pub struct ToolUpgradeArgs { } #[derive(Args)] -#[allow(clippy::struct_excessive_bools)] pub struct PythonNamespace { #[command(subcommand)] pub command: PythonCommand, @@ -4793,7 +4759,6 @@ pub enum PythonCommand { } #[derive(Args)] -#[allow(clippy::struct_excessive_bools)] pub struct PythonListArgs { /// A Python request to filter by. /// @@ -4848,7 +4813,6 @@ pub struct PythonListArgs { } #[derive(Args)] -#[allow(clippy::struct_excessive_bools)] pub struct PythonDirArgs { /// Show the directory into which `uv python` will install Python executables. /// @@ -4866,7 +4830,6 @@ pub struct PythonDirArgs { } #[derive(Args)] -#[allow(clippy::struct_excessive_bools)] pub struct PythonInstallArgs { /// The directory to store the Python installation in. /// @@ -4945,7 +4908,6 @@ pub struct PythonInstallArgs { } #[derive(Args)] -#[allow(clippy::struct_excessive_bools)] pub struct PythonUninstallArgs { /// The directory where the Python was installed. #[arg(long, short, env = EnvVars::UV_PYTHON_INSTALL_DIR)] @@ -4963,7 +4925,6 @@ pub struct PythonUninstallArgs { } #[derive(Args)] -#[allow(clippy::struct_excessive_bools)] pub struct PythonFindArgs { /// The Python request. /// @@ -5012,7 +4973,6 @@ pub struct PythonFindArgs { } #[derive(Args)] -#[allow(clippy::struct_excessive_bools)] pub struct PythonPinArgs { /// The Python version request. /// @@ -5061,7 +5021,6 @@ pub struct PythonPinArgs { } #[derive(Args)] -#[allow(clippy::struct_excessive_bools)] pub struct GenerateShellCompletionArgs { /// The shell to generate the completion script for pub shell: clap_complete_command::Shell, @@ -5100,7 +5059,6 @@ pub struct GenerateShellCompletionArgs { } #[derive(Args)] -#[allow(clippy::struct_excessive_bools)] pub struct IndexArgs { /// The URLs to use when resolving dependencies, in addition to the default index. /// @@ -5175,7 +5133,6 @@ pub struct IndexArgs { } #[derive(Args)] -#[allow(clippy::struct_excessive_bools)] pub struct RefreshArgs { /// Refresh all cached data. #[arg( @@ -5201,7 +5158,6 @@ pub struct RefreshArgs { } #[derive(Args)] -#[allow(clippy::struct_excessive_bools)] pub struct BuildOptionsArgs { /// Don't build source distributions. /// @@ -5257,7 +5213,6 @@ pub struct BuildOptionsArgs { /// Arguments that are used by commands that need to install (but not resolve) packages. #[derive(Args)] -#[allow(clippy::struct_excessive_bools)] pub struct InstallerArgs { #[command(flatten)] pub index_args: IndexArgs, @@ -5399,7 +5354,6 @@ pub struct InstallerArgs { /// Arguments that are used by commands that need to resolve (but not install) packages. #[derive(Args)] -#[allow(clippy::struct_excessive_bools)] pub struct ResolverArgs { #[command(flatten)] pub index_args: IndexArgs, @@ -5566,7 +5520,6 @@ pub struct ResolverArgs { /// Arguments that are used by commands that need to resolve and install packages. #[derive(Args)] -#[allow(clippy::struct_excessive_bools)] pub struct ResolverInstallerArgs { #[command(flatten)] pub index_args: IndexArgs, @@ -5783,7 +5736,6 @@ pub struct ResolverInstallerArgs { /// Arguments that are used by commands that need to fetch from the Simple API. #[derive(Args)] -#[allow(clippy::struct_excessive_bools)] pub struct FetchArgs { #[command(flatten)] pub index_args: IndexArgs, diff --git a/crates/uv-client/src/httpcache/control.rs b/crates/uv-client/src/httpcache/control.rs index 724683188..ddac9d1bc 100644 --- a/crates/uv-client/src/httpcache/control.rs +++ b/crates/uv-client/src/httpcache/control.rs @@ -21,7 +21,6 @@ use crate::rkyvutil::OwnedArchive; rkyv::Serialize, )] #[rkyv(derive(Debug))] -#[allow(clippy::struct_excessive_bools)] pub struct CacheControl { // directives for requests and responses /// * diff --git a/crates/uv-python/src/virtualenv.rs b/crates/uv-python/src/virtualenv.rs index 7d72188fc..ea578fff3 100644 --- a/crates/uv-python/src/virtualenv.rs +++ b/crates/uv-python/src/virtualenv.rs @@ -32,7 +32,6 @@ pub struct VirtualEnvironment { /// A parsed `pyvenv.cfg` #[derive(Debug, Clone)] -#[allow(clippy::struct_excessive_bools)] pub struct PyVenvConfiguration { /// Was the virtual environment created with the `virtualenv` package? pub(crate) virtualenv: bool, diff --git a/crates/uv-resolver/src/resolution/display.rs b/crates/uv-resolver/src/resolution/display.rs index 2f70f00f6..318fb4e54 100644 --- a/crates/uv-resolver/src/resolution/display.rs +++ b/crates/uv-resolver/src/resolution/display.rs @@ -14,7 +14,6 @@ use crate::{ResolverEnvironment, ResolverOutput}; /// A [`std::fmt::Display`] implementation for the resolution graph. #[derive(Debug)] -#[allow(clippy::struct_excessive_bools)] pub struct DisplayResolutionGraph<'a> { /// The underlying graph. resolution: &'a ResolverOutput, diff --git a/crates/uv-resolver/src/version_map.rs b/crates/uv-resolver/src/version_map.rs index 8a0b17fc4..63132ad0d 100644 --- a/crates/uv-resolver/src/version_map.rs +++ b/crates/uv-resolver/src/version_map.rs @@ -345,7 +345,6 @@ struct VersionMapEager { /// avoiding another conversion step into a fully filled out `VersionMap` can /// provide substantial savings in some cases. #[derive(Debug)] -#[allow(clippy::struct_excessive_bools)] struct VersionMapLazy { /// A map from version to possibly-initialized distribution. map: BTreeMap, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index f8d44b50c..fb1a62b41 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -54,7 +54,6 @@ use crate::commands::{InitKind, InitProjectKind, pip::operations::Modifications} const PYPI_PUBLISH_URL: &str = "https://upload.pypi.org/legacy/"; /// The resolved global settings to use for any invocation of the CLI. -#[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone)] pub(crate) struct GlobalSettings { pub(crate) required_version: Option, @@ -199,7 +198,6 @@ impl NetworkSettings { } /// The resolved cache settings to use for any invocation of the CLI. -#[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone)] pub(crate) struct CacheSettings { pub(crate) no_cache: bool, @@ -222,7 +220,6 @@ impl CacheSettings { } /// The resolved settings to use for a `init` invocation. -#[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone)] pub(crate) struct InitSettings { pub(crate) path: Option, @@ -307,7 +304,6 @@ impl InitSettings { } /// The resolved settings to use for a `run` invocation. -#[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone)] pub(crate) struct RunSettings { pub(crate) locked: bool, @@ -454,7 +450,6 @@ impl RunSettings { } /// The resolved settings to use for a `tool run` invocation. -#[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone)] pub(crate) struct ToolRunSettings { pub(crate) command: Option, @@ -586,7 +581,6 @@ impl ToolRunSettings { } /// The resolved settings to use for a `tool install` invocation. -#[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone)] pub(crate) struct ToolInstallSettings { pub(crate) package: String, @@ -681,7 +675,6 @@ impl ToolInstallSettings { } /// The resolved settings to use for a `tool upgrade` invocation. -#[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone)] pub(crate) struct ToolUpgradeSettings { pub(crate) names: Vec, @@ -776,7 +769,6 @@ impl ToolUpgradeSettings { } /// The resolved settings to use for a `tool list` invocation. -#[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone)] pub(crate) struct ToolListSettings { pub(crate) show_paths: bool, @@ -808,7 +800,6 @@ impl ToolListSettings { } /// The resolved settings to use for a `tool uninstall` invocation. -#[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone)] pub(crate) struct ToolUninstallSettings { pub(crate) name: Vec, @@ -827,7 +818,6 @@ impl ToolUninstallSettings { } /// The resolved settings to use for a `tool dir` invocation. -#[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone)] pub(crate) struct ToolDirSettings { pub(crate) bin: bool, @@ -854,7 +844,6 @@ pub(crate) enum PythonListKinds { } /// The resolved settings to use for a `tool run` invocation. -#[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone)] pub(crate) struct PythonListSettings { pub(crate) request: Option, @@ -914,7 +903,6 @@ impl PythonListSettings { } /// The resolved settings to use for a `python dir` invocation. -#[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone)] pub(crate) struct PythonDirSettings { pub(crate) bin: bool, @@ -931,7 +919,6 @@ impl PythonDirSettings { } /// The resolved settings to use for a `python install` invocation. -#[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone)] pub(crate) struct PythonInstallSettings { pub(crate) install_dir: Option, @@ -987,7 +974,6 @@ impl PythonInstallSettings { } /// The resolved settings to use for a `python uninstall` invocation. -#[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone)] pub(crate) struct PythonUninstallSettings { pub(crate) install_dir: Option, @@ -1017,7 +1003,6 @@ impl PythonUninstallSettings { } /// The resolved settings to use for a `python find` invocation. -#[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone)] pub(crate) struct PythonFindSettings { pub(crate) request: Option, @@ -1049,7 +1034,6 @@ impl PythonFindSettings { } /// The resolved settings to use for a `python pin` invocation. -#[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone)] pub(crate) struct PythonPinSettings { pub(crate) request: Option, @@ -1575,7 +1559,6 @@ impl VersionSettings { } /// The resolved settings to use for a `tree` invocation. -#[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone)] pub(crate) struct TreeSettings { pub(crate) groups: DependencyGroups, @@ -1771,7 +1754,6 @@ impl ExportSettings { } /// The resolved settings to use for a `pip compile` invocation. -#[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone)] pub(crate) struct PipCompileSettings { pub(crate) format: Option, @@ -1949,7 +1931,6 @@ impl PipCompileSettings { } /// The resolved settings to use for a `pip sync` invocation. -#[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone)] pub(crate) struct PipSyncSettings { pub(crate) src_file: Vec, @@ -2036,7 +2017,6 @@ impl PipSyncSettings { } /// The resolved settings to use for a `pip install` invocation. -#[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone)] pub(crate) struct PipInstallSettings { pub(crate) package: Vec, @@ -2195,7 +2175,6 @@ impl PipInstallSettings { } /// The resolved settings to use for a `pip uninstall` invocation. -#[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone)] pub(crate) struct PipUninstallSettings { pub(crate) package: Vec, @@ -2243,7 +2222,6 @@ impl PipUninstallSettings { } /// The resolved settings to use for a `pip freeze` invocation. -#[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone)] pub(crate) struct PipFreezeSettings { pub(crate) exclude_editable: bool, @@ -2282,7 +2260,6 @@ impl PipFreezeSettings { } /// The resolved settings to use for a `pip list` invocation. -#[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone)] pub(crate) struct PipListSettings { pub(crate) editable: Option, @@ -2330,7 +2307,6 @@ impl PipListSettings { } /// The resolved settings to use for a `pip show` invocation. -#[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone)] pub(crate) struct PipShowSettings { pub(crate) package: Vec, @@ -2369,7 +2345,6 @@ impl PipShowSettings { } /// The resolved settings to use for a `pip tree` invocation. -#[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone)] pub(crate) struct PipTreeSettings { pub(crate) show_version_specifiers: bool, @@ -2419,7 +2394,6 @@ impl PipTreeSettings { } /// The resolved settings to use for a `pip check` invocation. -#[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone)] pub(crate) struct PipCheckSettings { pub(crate) settings: PipSettings, @@ -2448,7 +2422,6 @@ impl PipCheckSettings { } /// The resolved settings to use for a `build` invocation. -#[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone)] pub(crate) struct BuildSettings { pub(crate) src: Option, @@ -2525,7 +2498,6 @@ impl BuildSettings { } /// The resolved settings to use for a `venv` invocation. -#[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone)] pub(crate) struct VenvSettings { pub(crate) seed: bool, @@ -2612,7 +2584,6 @@ pub(crate) struct InstallerSettingsRef<'a> { /// /// Combines the `[tool.uv]` persistent configuration with the command-line arguments /// ([`ResolverArgs`], represented as [`ResolverOptions`]). -#[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone, Default)] pub(crate) struct ResolverSettings { pub(crate) build_options: BuildOptions, @@ -2702,7 +2673,6 @@ impl From for ResolverSettings { /// /// Represents the shared settings that are used across all uv commands outside the `pip` API. /// Analogous to the settings contained in the `[tool.uv]` table, combined with [`ResolverInstallerArgs`]. -#[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone, Default)] pub(crate) struct ResolverInstallerSettings { pub(crate) resolver: ResolverSettings, @@ -2792,7 +2762,6 @@ impl From for ResolverInstallerSettings { /// /// Represents the shared settings that are used across all `pip` commands. Analogous to the /// settings contained in the `[tool.uv.pip]` table. -#[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone)] pub(crate) struct PipSettings { pub(crate) index_locations: IndexLocations, @@ -3169,7 +3138,6 @@ impl<'a> From<&'a ResolverInstallerSettings> for InstallerSettingsRef<'a> { } /// The resolved settings to use for an invocation of the `uv publish` CLI. -#[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone)] pub(crate) struct PublishSettings { // CLI only, see [`PublishArgs`] for docs. From d653fbb1331cfd0bb8f937402c93f95e2e331f82 Mon Sep 17 00:00:00 2001 From: FishAlchemist <48265002+FishAlchemist@users.noreply.github.com> Date: Tue, 17 Jun 2025 21:55:03 +0800 Subject: [PATCH 033/185] doc: Sync PyTorch integration index for CUDA and ROCm versions from PyTorch website. (#14100) ## Summary Just to sync the documentation with PyTorch's officially recommended installation method. ![image](https://github.com/user-attachments/assets/7be0aea6-b51b-4083-acae-fbe8aa79c363) **Change:** * CUDA 12.1 to CUDA 12.6 * CUDA 12.4 to CUDA 12.8 * ROCm 6.2 to ROCm 6.3 ## Test Plan Run doc server in local. Result:
CUDA 12.6

![image](https://github.com/user-attachments/assets/962a1058-3922-4709-a57f-200939d0a397) ![image](https://github.com/user-attachments/assets/0dae693f-2dc1-4d3e-9186-d26aa84c7d73)

CUDA 12.8

![image](https://github.com/user-attachments/assets/dc12d09b-f8f2-41d3-b185-cb1e4e8b062b) ![image](https://github.com/user-attachments/assets/1b5490e3-6265-4bad-8af2-eab0a207adab)

ROCm6

![image](https://github.com/user-attachments/assets/3b17a24f-3210-4a99-a81b-f8f2f5578b73) ![image](https://github.com/user-attachments/assets/c7baae55-14e5-45d0-94a0-164ab31bbffc)

--- docs/guides/integration/pytorch.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/guides/integration/pytorch.md b/docs/guides/integration/pytorch.md index 79b6f31e1..7fd0f8004 100644 --- a/docs/guides/integration/pytorch.md +++ b/docs/guides/integration/pytorch.md @@ -85,21 +85,21 @@ In such cases, the first step is to add the relevant PyTorch index to your `pypr explicit = true ``` -=== "CUDA 12.1" +=== "CUDA 12.6" ```toml [[tool.uv.index]] - name = "pytorch-cu121" - url = "https://download.pytorch.org/whl/cu121" + name = "pytorch-cu126" + url = "https://download.pytorch.org/whl/cu126" explicit = true ``` -=== "CUDA 12.4" +=== "CUDA 12.8" ```toml [[tool.uv.index]] - name = "pytorch-cu124" - url = "https://download.pytorch.org/whl/cu124" + name = "pytorch-cu128" + url = "https://download.pytorch.org/whl/cu128" explicit = true ``` @@ -108,7 +108,7 @@ In such cases, the first step is to add the relevant PyTorch index to your `pypr ```toml [[tool.uv.index]] name = "pytorch-rocm" - url = "https://download.pytorch.org/whl/rocm6.2" + url = "https://download.pytorch.org/whl/rocm6.3" explicit = true ``` @@ -154,7 +154,7 @@ Next, update the `pyproject.toml` to point `torch` and `torchvision` to the desi ] ``` -=== "CUDA 12.1" +=== "CUDA 12.6" PyTorch doesn't publish CUDA builds for macOS. As such, we gate on `sys_platform` to instruct uv to limit the PyTorch index to Linux and Windows, falling back to PyPI on macOS: @@ -162,14 +162,14 @@ Next, update the `pyproject.toml` to point `torch` and `torchvision` to the desi ```toml [tool.uv.sources] torch = [ - { index = "pytorch-cu121", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { index = "pytorch-cu126", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] torchvision = [ - { index = "pytorch-cu121", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { index = "pytorch-cu126", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] ``` -=== "CUDA 12.4" +=== "CUDA 12.8" PyTorch doesn't publish CUDA builds for macOS. As such, we gate on `sys_platform` to instruct uv to limit the PyTorch index to Linux and Windows, falling back to PyPI on macOS: @@ -177,10 +177,10 @@ Next, update the `pyproject.toml` to point `torch` and `torchvision` to the desi ```toml [tool.uv.sources] torch = [ - { index = "pytorch-cu124", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { index = "pytorch-cu128", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] torchvision = [ - { index = "pytorch-cu124", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { index = "pytorch-cu128", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] ``` From 3d4f0c934e8f663a9be3c72af7c0a32111b9567e Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Tue, 17 Jun 2025 11:50:05 -0400 Subject: [PATCH 034/185] Fix handling of changes to `requires-python` (#14076) When using `uv lock --upgrade-package=python` after changing `requires-python`, it was possible to get into a state where the fork markers produced corresponded to the empty set. This in turn resulted in an empty lock file. There was already some infrastructure in place that I think was perhaps intended to handle this. In particular, `Lock::check_marker_coverage` checks whether the fork markers have some overlap with the supported environments (including the `requires-python`). But there were two problems with this. First is that in lock validation, this marker coverage check came _after_ a path that returned `Preferable` (meaning that the fork markers should be kept) when `--upgrade-package` was used. Second is that the marker coverage check used the `requires-python` in the lock file and _not_ the `requires-python` in the now updated `pyproject.toml`. We attempt to solve this conundrum by slightly re-arranging lock file validation and by explicitly checking whether the *new* `requires-python` is disjoint from the fork markers in the lock file. If it is, then we return `Versions` from lock file validation (indicating that the fork markers should be dropped). Fixes #13951 --- crates/uv-resolver/src/lock/mod.rs | 30 +++++ crates/uv/src/commands/project/lock.rs | 62 ++++++--- crates/uv/tests/it/lock.rs | 169 ++++++++++++++++++++++++- crates/uv/tests/it/sync.rs | 12 +- 4 files changed, 250 insertions(+), 23 deletions(-) diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index 47597f2ec..5af4377b9 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -768,6 +768,36 @@ impl Lock { } } + /// Checks whether the new requires-python specification is disjoint with + /// the fork markers in this lock file. + /// + /// If they are disjoint, then the union of the fork markers along with the + /// given requires-python specification (converted to a marker tree) are + /// returned. + /// + /// When disjoint, the fork markers in the lock file should be dropped and + /// not used. + pub fn requires_python_coverage( + &self, + new_requires_python: &RequiresPython, + ) -> Result<(), (MarkerTree, MarkerTree)> { + let fork_markers_union = if self.fork_markers().is_empty() { + self.requires_python.to_marker_tree() + } else { + let mut fork_markers_union = MarkerTree::FALSE; + for fork_marker in self.fork_markers() { + fork_markers_union.or(fork_marker.pep508()); + } + fork_markers_union + }; + let new_requires_python = new_requires_python.to_marker_tree(); + if fork_markers_union.is_disjoint(new_requires_python) { + Err((fork_markers_union, new_requires_python)) + } else { + Ok(()) + } + } + /// Returns the TOML representation of this lockfile. pub fn to_toml(&self) -> Result { // Catch a lockfile where the union of fork markers doesn't cover the supported diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index b57df429b..1ab4441b0 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -983,13 +983,54 @@ impl ValidatedLock { return Ok(Self::Unusable(lock)); } Upgrade::Packages(_) => { - // If the user specified `--upgrade-package`, then at best we can prefer some of - // the existing versions. - debug!("Ignoring existing lockfile due to `--upgrade-package`"); - return Ok(Self::Preferable(lock)); + // This is handled below, after some checks regarding fork + // markers. In particular, we'd like to return `Preferable` + // here, but we shouldn't if the fork markers cannot be + // reused. } } + // NOTE: It's important that this appears before any possible path that + // returns `Self::Preferable`. In particular, if our fork markers are + // bunk, then we shouldn't return a result that indicates we should try + // to re-use the existing fork markers. + if let Err((fork_markers_union, environments_union)) = lock.check_marker_coverage() { + warn_user!( + "Ignoring existing lockfile due to fork markers not covering the supported environments: `{}` vs `{}`", + fork_markers_union + .try_to_string() + .unwrap_or("true".to_string()), + environments_union + .try_to_string() + .unwrap_or("true".to_string()), + ); + return Ok(Self::Versions(lock)); + } + + // NOTE: Similarly as above, this should also appear before any + // possible code path that can return `Self::Preferable`. + if let Err((fork_markers_union, requires_python_marker)) = + lock.requires_python_coverage(requires_python) + { + warn_user!( + "Ignoring existing lockfile due to fork markers being disjoint with `requires-python`: `{}` vs `{}`", + fork_markers_union + .try_to_string() + .unwrap_or("true".to_string()), + requires_python_marker + .try_to_string() + .unwrap_or("true".to_string()), + ); + return Ok(Self::Versions(lock)); + } + + if let Upgrade::Packages(_) = upgrade { + // If the user specified `--upgrade-package`, then at best we can prefer some of + // the existing versions. + debug!("Ignoring existing lockfile due to `--upgrade-package`"); + return Ok(Self::Preferable(lock)); + } + // If the Requires-Python bound has changed, we have to perform a clean resolution, since // the set of `resolution-markers` may no longer cover the entire supported Python range. if lock.requires_python().range() != requires_python.range() { @@ -1022,19 +1063,6 @@ impl ValidatedLock { return Ok(Self::Versions(lock)); } - if let Err((fork_markers_union, environments_union)) = lock.check_marker_coverage() { - warn_user!( - "Ignoring existing lockfile due to fork markers not covering the supported environments: `{}` vs `{}`", - fork_markers_union - .try_to_string() - .unwrap_or("true".to_string()), - environments_union - .try_to_string() - .unwrap_or("true".to_string()), - ); - return Ok(Self::Versions(lock)); - } - // If the set of required platforms has changed, we have to perform a clean resolution. let expected = lock.simplified_required_environments(); let actual = required_environments diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index e9615538e..ac20124a0 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -4731,15 +4731,16 @@ fn lock_requires_python_wheels() -> Result<()> { "#, )?; - uv_snapshot!(context.filters(), context.lock(), @r###" + uv_snapshot!(context.filters(), context.lock(), @r" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Using CPython 3.11.[X] interpreter at: [PYTHON-3.11] + warning: Ignoring existing lockfile due to fork markers being disjoint with `requires-python`: `python_full_version == '3.12.*'` vs `python_full_version == '3.11.*'` Resolved 2 packages in [TIME] - "###); + "); let lock = fs_err::read_to_string(&lockfile).unwrap(); @@ -28020,6 +28021,170 @@ fn lock_conflict_for_disjoint_python_version() -> Result<()> { Ok(()) } +/// Check that we hint if the resolution failed for a different platform. +#[cfg(feature = "python-patch")] +#[test] +fn lock_requires_python_empty_lock_file() -> Result<()> { + // N.B. These versions were selected based on what was + // in `.python-versions` at the time of writing (2025-06-16). + let (v1, v2) = ("3.13.0", "3.13.2"); + let context = TestContext::new_with_versions(&[v1, v2]); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(&format!( + r#" + [project] + name = "renovate-bug-repro" + version = "0.1.0" + requires-python = "=={v1}" + dependencies = ["opencv-python-headless>=4.8"] + "#, + ))?; + + uv_snapshot!(context.filters(), context.lock(), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.13.0 interpreter at: [PYTHON-3.13.0] + Resolved 3 packages in [TIME] + "); + + let lock = context.read("uv.lock"); + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r#" + version = 1 + revision = 2 + requires-python = "==3.13.0" + resolution-markers = [ + "sys_platform == 'darwin'", + "platform_machine == 'aarch64' and sys_platform == 'linux'", + "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')", + ] + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [[package]] + name = "numpy" + version = "1.26.4" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/65/6e/09db70a523a96d25e115e71cc56a6f9031e7b8cd166c1ac8438307c14058/numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", size = 15786129, upload-time = "2024-02-06T00:26:44.495Z" } + + [[package]] + name = "opencv-python-headless" + version = "4.9.0.80" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "numpy" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/3d/b2/c308bc696bf5d75304175c62222ec8af9a6d5cfe36c14f19f15ea9d1a132/opencv-python-headless-4.9.0.80.tar.gz", hash = "sha256:71a4cd8cf7c37122901d8e81295db7fb188730e33a0e40039a4e59c1030b0958", size = 92910044, upload-time = "2023-12-31T13:34:50.518Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/42/da433fca5733a3ce7e88dd0d4018f70dcffaf48770b5142250815f4faddb/opencv_python_headless-4.9.0.80-cp37-abi3-macosx_10_16_x86_64.whl", hash = "sha256:2ea8a2edc4db87841991b2fbab55fc07b97ecb602e0f47d5d485bd75cee17c1a", size = 55689478, upload-time = "2023-12-31T14:31:30.476Z" }, + { url = "https://files.pythonhosted.org/packages/32/0c/a59f2a40d6058ee8126668dc5dff6977c913f6ecd21dbd15b41563409a18/opencv_python_headless-4.9.0.80-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:e0ee54e27be493e8f7850847edae3128e18b540dac1d7b2e4001b8944e11e1c6", size = 35354670, upload-time = "2023-12-31T16:38:31.588Z" }, + { url = "https://files.pythonhosted.org/packages/36/37/225a1f8be42610ffecf677558311ab0f9dfdc63537c250a2bce76762a380/opencv_python_headless-4.9.0.80-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57ce2865e8fec431c6f97a81e9faaf23fa5be61011d0a75ccf47a3c0d65fa73d", size = 28954368, upload-time = "2023-12-31T16:40:00.838Z" }, + { url = "https://files.pythonhosted.org/packages/71/19/3c65483a80a1d062d46ae20faf5404712d25cb1dfdcaf371efbd67c38544/opencv_python_headless-4.9.0.80-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:976656362d68d9f40a5c66f83901430538002465f7db59142784f3893918f3df", size = 49591873, upload-time = "2023-12-31T13:34:44.316Z" }, + { url = "https://files.pythonhosted.org/packages/10/98/300382ff6ddff3a487e808c8a76362e430f5016002fcbefb3b3117aad32b/opencv_python_headless-4.9.0.80-cp37-abi3-win32.whl", hash = "sha256:11e3849d83e6651d4e7699aadda9ec7ed7c38957cbbcb99db074f2a2d2de9670", size = 28488841, upload-time = "2023-12-31T13:34:31.974Z" }, + { url = "https://files.pythonhosted.org/packages/20/44/458a0a135866f5e08266566b32ad9a182a7a059a894effe6c41a9c841ff1/opencv_python_headless-4.9.0.80-cp37-abi3-win_amd64.whl", hash = "sha256:a8056c2cb37cd65dfcdf4153ca16f7362afcf3a50d600d6bb69c660fc61ee29c", size = 38536073, upload-time = "2023-12-31T13:34:39.675Z" }, + ] + + [[package]] + name = "renovate-bug-repro" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "opencv-python-headless" }, + ] + + [package.metadata] + requires-dist = [{ name = "opencv-python-headless", specifier = ">=4.8" }] + "# + ); + }); + + pyproject_toml.write_str(&format!( + r#" + [project] + name = "renovate-bug-repro" + version = "0.1.0" + requires-python = "=={v2}" + dependencies = ["opencv-python-headless>=4.8"] + "#, + ))?; + + uv_snapshot!(context.filters(), context.lock().arg("--upgrade-package=python"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.13.2 interpreter at: [PYTHON-3.13.2] + warning: Ignoring existing lockfile due to fork markers being disjoint with `requires-python`: `python_full_version == '3.13.0'` vs `python_full_version == '3.13.2'` + Resolved 3 packages in [TIME] + "); + + let lock = context.read("uv.lock"); + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r#" + version = 1 + revision = 2 + requires-python = "==3.13.2" + resolution-markers = [ + "sys_platform == 'darwin'", + "platform_machine == 'aarch64' and sys_platform == 'linux'", + "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')", + ] + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [[package]] + name = "numpy" + version = "1.26.4" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/65/6e/09db70a523a96d25e115e71cc56a6f9031e7b8cd166c1ac8438307c14058/numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", size = 15786129, upload-time = "2024-02-06T00:26:44.495Z" } + + [[package]] + name = "opencv-python-headless" + version = "4.9.0.80" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "numpy" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/3d/b2/c308bc696bf5d75304175c62222ec8af9a6d5cfe36c14f19f15ea9d1a132/opencv-python-headless-4.9.0.80.tar.gz", hash = "sha256:71a4cd8cf7c37122901d8e81295db7fb188730e33a0e40039a4e59c1030b0958", size = 92910044, upload-time = "2023-12-31T13:34:50.518Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/42/da433fca5733a3ce7e88dd0d4018f70dcffaf48770b5142250815f4faddb/opencv_python_headless-4.9.0.80-cp37-abi3-macosx_10_16_x86_64.whl", hash = "sha256:2ea8a2edc4db87841991b2fbab55fc07b97ecb602e0f47d5d485bd75cee17c1a", size = 55689478, upload-time = "2023-12-31T14:31:30.476Z" }, + { url = "https://files.pythonhosted.org/packages/32/0c/a59f2a40d6058ee8126668dc5dff6977c913f6ecd21dbd15b41563409a18/opencv_python_headless-4.9.0.80-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:e0ee54e27be493e8f7850847edae3128e18b540dac1d7b2e4001b8944e11e1c6", size = 35354670, upload-time = "2023-12-31T16:38:31.588Z" }, + { url = "https://files.pythonhosted.org/packages/36/37/225a1f8be42610ffecf677558311ab0f9dfdc63537c250a2bce76762a380/opencv_python_headless-4.9.0.80-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57ce2865e8fec431c6f97a81e9faaf23fa5be61011d0a75ccf47a3c0d65fa73d", size = 28954368, upload-time = "2023-12-31T16:40:00.838Z" }, + { url = "https://files.pythonhosted.org/packages/71/19/3c65483a80a1d062d46ae20faf5404712d25cb1dfdcaf371efbd67c38544/opencv_python_headless-4.9.0.80-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:976656362d68d9f40a5c66f83901430538002465f7db59142784f3893918f3df", size = 49591873, upload-time = "2023-12-31T13:34:44.316Z" }, + { url = "https://files.pythonhosted.org/packages/10/98/300382ff6ddff3a487e808c8a76362e430f5016002fcbefb3b3117aad32b/opencv_python_headless-4.9.0.80-cp37-abi3-win32.whl", hash = "sha256:11e3849d83e6651d4e7699aadda9ec7ed7c38957cbbcb99db074f2a2d2de9670", size = 28488841, upload-time = "2023-12-31T13:34:31.974Z" }, + { url = "https://files.pythonhosted.org/packages/20/44/458a0a135866f5e08266566b32ad9a182a7a059a894effe6c41a9c841ff1/opencv_python_headless-4.9.0.80-cp37-abi3-win_amd64.whl", hash = "sha256:a8056c2cb37cd65dfcdf4153ca16f7362afcf3a50d600d6bb69c660fc61ee29c", size = 38536073, upload-time = "2023-12-31T13:34:39.675Z" }, + ] + + [[package]] + name = "renovate-bug-repro" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "opencv-python-headless" }, + ] + + [package.metadata] + requires-dist = [{ name = "opencv-python-headless", specifier = ">=4.8" }] + "# + ); + }); + + Ok(()) +} + /// Check that we hint if the resolution failed for a different platform. #[test] fn lock_conflict_for_disjoint_platform() -> Result<()> { diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index 70d8a9118..966dd41d2 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -8140,7 +8140,7 @@ fn sync_dry_run() -> Result<()> { "#, )?; - uv_snapshot!(context.filters(), context.sync().arg("--dry-run"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--dry-run"), @r" success: true exit_code: 0 ----- stdout ----- @@ -8148,14 +8148,15 @@ fn sync_dry_run() -> Result<()> { ----- stderr ----- Using CPython 3.9.[X] interpreter at: [PYTHON-3.9] Would replace existing virtual environment at: .venv + warning: Ignoring existing lockfile due to fork markers being disjoint with `requires-python`: `python_full_version >= '3.12'` vs `python_full_version == '3.9.*'` Resolved 2 packages in [TIME] Would update lockfile at: uv.lock Would install 1 package + iniconfig==2.0.0 - "###); + "); // Perform a full sync. - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 ----- stdout ----- @@ -8164,10 +8165,11 @@ fn sync_dry_run() -> Result<()> { Using CPython 3.9.[X] interpreter at: [PYTHON-3.9] Removed virtual environment at: .venv Creating virtual environment at: .venv + warning: Ignoring existing lockfile due to fork markers being disjoint with `requires-python`: `python_full_version >= '3.12'` vs `python_full_version == '3.9.*'` Resolved 2 packages in [TIME] Installed 1 package in [TIME] + iniconfig==2.0.0 - "###); + "); let output = context.sync().arg("--dry-run").arg("-vv").output()?; let stderr = String::from_utf8_lossy(&output.stderr); @@ -8658,6 +8660,7 @@ fn sync_locked_script() -> Result<()> { ----- stderr ----- Recreating script environment at: [CACHE_DIR]/environments-v2/script-[HASH] + warning: Ignoring existing lockfile due to fork markers being disjoint with `requires-python`: `python_full_version >= '3.11'` vs `python_full_version >= '3.8' and python_full_version < '3.11'` Resolved 6 packages in [TIME] error: The lockfile at `uv.lock` needs to be updated, but `--locked` was provided. To update the lockfile, run `uv lock`. "); @@ -8669,6 +8672,7 @@ fn sync_locked_script() -> Result<()> { ----- stderr ----- Using script environment at: [CACHE_DIR]/environments-v2/script-[HASH] + warning: Ignoring existing lockfile due to fork markers being disjoint with `requires-python`: `python_full_version >= '3.11'` vs `python_full_version >= '3.8' and python_full_version < '3.11'` Resolved 6 packages in [TIME] Prepared 2 packages in [TIME] Installed 6 packages in [TIME] From 10e1d17cfce45c3759bc232fc270af993f9812cd Mon Sep 17 00:00:00 2001 From: konsti Date: Tue, 17 Jun 2025 19:18:08 +0200 Subject: [PATCH 035/185] Don't use walrus operator in interpreter query script (#14108) Fix `uv run -p 3.7` by not using a walrus operator. Python 3.7 isn't really supported anymore, but there's no reason to break interpreter discovery for it. --- crates/uv-python/python/packaging/_manylinux.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/uv-python/python/packaging/_manylinux.py b/crates/uv-python/python/packaging/_manylinux.py index ea7125c76..a0e8846e7 100644 --- a/crates/uv-python/python/packaging/_manylinux.py +++ b/crates/uv-python/python/packaging/_manylinux.py @@ -255,5 +255,6 @@ def platform_tags(archs: Sequence[str]) -> Iterator[str]: if _is_compatible(arch, glibc_version): yield "manylinux_{}_{}_{}".format(*glibc_version, arch) # Handle the legacy manylinux1, manylinux2010, manylinux2014 tags. - if legacy_tag := _LEGACY_MANYLINUX_MAP.get(glibc_version): + legacy_tag = _LEGACY_MANYLINUX_MAP.get(glibc_version) + if legacy_tag: yield f"{legacy_tag}_{arch}" From c25c800367a5f43069f1c9d778cfd5de1bcc54a6 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Tue, 17 Jun 2025 12:28:23 -0500 Subject: [PATCH 036/185] Fix Ruff linting (#14111) --- crates/uv-build/ruff.toml | 2 ++ crates/uv-python/fetch-download-metadata.py | 4 +++- crates/uv-python/python/get_interpreter_info.py | 13 +++++++------ crates/uv-python/python/packaging/_elffile.py | 3 +-- crates/uv-python/python/packaging/_manylinux.py | 3 +-- crates/uv-python/python/ruff.toml | 2 ++ ruff.toml | 4 ++-- scripts/transform_readme.py | 3 +-- .../albatross-in-example/src/albatross/__init__.py | 2 +- 9 files changed, 20 insertions(+), 16 deletions(-) create mode 100644 crates/uv-build/ruff.toml create mode 100644 crates/uv-python/python/ruff.toml diff --git a/crates/uv-build/ruff.toml b/crates/uv-build/ruff.toml new file mode 100644 index 000000000..e480507a2 --- /dev/null +++ b/crates/uv-build/ruff.toml @@ -0,0 +1,2 @@ +# It is important retain compatibility with old versions in the build backend +target-version = "py37" diff --git a/crates/uv-python/fetch-download-metadata.py b/crates/uv-python/fetch-download-metadata.py index 0b5d9caec..08adaecea 100755 --- a/crates/uv-python/fetch-download-metadata.py +++ b/crates/uv-python/fetch-download-metadata.py @@ -630,7 +630,9 @@ class GraalPyFinder(Finder): for download in batch: url = download.url + ".sha256" checksum_requests.append(self.client.get(url)) - for download, resp in zip(batch, await asyncio.gather(*checksum_requests)): + for download, resp in zip( + batch, await asyncio.gather(*checksum_requests), strict=False + ): try: resp.raise_for_status() except httpx.HTTPStatusError as e: diff --git a/crates/uv-python/python/get_interpreter_info.py b/crates/uv-python/python/get_interpreter_info.py index 0fe088819..8e9fc37fd 100644 --- a/crates/uv-python/python/get_interpreter_info.py +++ b/crates/uv-python/python/get_interpreter_info.py @@ -39,10 +39,9 @@ if hasattr(sys, "implementation"): # GraalPy reports the CPython version as sys.implementation.version, # so we need to discover the GraalPy version from the cache_tag import re + implementation_version = re.sub( - r"graalpy(\d)(\d+)-\d+", - r"\1.\2", - sys.implementation.cache_tag + r"graalpy(\d)(\d+)-\d+", r"\1.\2", sys.implementation.cache_tag ) else: implementation_version = format_full_version(sys.implementation.version) @@ -583,7 +582,6 @@ def main() -> None: elif os_and_arch["os"]["name"] == "musllinux": manylinux_compatible = True - # By default, pip uses sysconfig on Python 3.10+. # But Python distributors can override this decision by setting: # sysconfig._PIP_USE_SYSCONFIG = True / False @@ -608,7 +606,7 @@ def main() -> None: except (ImportError, AttributeError): pass - import distutils.dist + import distutils.dist # noqa: F401 except ImportError: # We require distutils, but it's not installed; this is fairly # common in, e.g., deadsnakes where distutils is packaged @@ -641,7 +639,10 @@ def main() -> None: # Prior to the introduction of `sysconfig` patching, python-build-standalone installations would always use # "/install" as the prefix. With `sysconfig` patching, we rewrite the prefix to match the actual installation # location. So in newer versions, we also write a dedicated flag to indicate standalone builds. - "standalone": sysconfig.get_config_var("prefix") == "/install" or bool(sysconfig.get_config_var("PYTHON_BUILD_STANDALONE")), + "standalone": ( + sysconfig.get_config_var("prefix") == "/install" + or bool(sysconfig.get_config_var("PYTHON_BUILD_STANDALONE")) + ), "scheme": get_scheme(use_sysconfig_scheme), "virtualenv": get_virtualenv(), "platform": os_and_arch, diff --git a/crates/uv-python/python/packaging/_elffile.py b/crates/uv-python/python/packaging/_elffile.py index f7a02180b..8dc7fb32a 100644 --- a/crates/uv-python/python/packaging/_elffile.py +++ b/crates/uv-python/python/packaging/_elffile.py @@ -69,8 +69,7 @@ class ELFFile: }[(self.capacity, self.encoding)] except KeyError: raise ELFInvalid( - f"unrecognized capacity ({self.capacity}) or " - f"encoding ({self.encoding})" + f"unrecognized capacity ({self.capacity}) or encoding ({self.encoding})" ) try: diff --git a/crates/uv-python/python/packaging/_manylinux.py b/crates/uv-python/python/packaging/_manylinux.py index a0e8846e7..7b52a5581 100644 --- a/crates/uv-python/python/packaging/_manylinux.py +++ b/crates/uv-python/python/packaging/_manylinux.py @@ -161,8 +161,7 @@ def _parse_glibc_version(version_str: str) -> _GLibCVersion: m = re.match(r"(?P[0-9]+)\.(?P[0-9]+)", version_str) if not m: warnings.warn( - f"Expected glibc version with 2 components major.minor," - f" got: {version_str}", + f"Expected glibc version with 2 components major.minor, got: {version_str}", RuntimeWarning, ) return _GLibCVersion(-1, -1) diff --git a/crates/uv-python/python/ruff.toml b/crates/uv-python/python/ruff.toml new file mode 100644 index 000000000..5e6921be4 --- /dev/null +++ b/crates/uv-python/python/ruff.toml @@ -0,0 +1,2 @@ +# It is important retain compatibility when querying interpreters +target-version = "py37" diff --git a/ruff.toml b/ruff.toml index 8aac77263..7c6488a1e 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,10 +1,10 @@ -target-version = "py37" +target-version = "py312" exclude = [ "crates/uv-virtualenv/src/activator/activate_this.py", "crates/uv-virtualenv/src/_virtualenv.py", - "crates/uv-python/python", "ecosystem", "scripts/workspaces", + "scripts/packages", ] [lint] diff --git a/scripts/transform_readme.py b/scripts/transform_readme.py index b8f11fbe0..9d52d6517 100644 --- a/scripts/transform_readme.py +++ b/scripts/transform_readme.py @@ -9,11 +9,10 @@ from __future__ import annotations import argparse import re +import tomllib import urllib.parse from pathlib import Path -import tomllib - # To be kept in sync with: `docs/index.md` URL = "https://github.com/astral-sh/uv/assets/1309177/{}" URL_LIGHT = URL.format("629e59c0-9c6e-4013-9ad4-adb2bcf5080d") diff --git a/scripts/workspaces/albatross-in-example/src/albatross/__init__.py b/scripts/workspaces/albatross-in-example/src/albatross/__init__.py index d79aed9cb..764c5ce3f 100644 --- a/scripts/workspaces/albatross-in-example/src/albatross/__init__.py +++ b/scripts/workspaces/albatross-in-example/src/albatross/__init__.py @@ -5,5 +5,5 @@ def fly(): pass -if __name__ == '__main__': +if __name__ == "__main__": print("Caw") From 8808e67cff7b6c2a4320a40322d80931d2724be5 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Tue, 17 Jun 2025 15:04:39 -0500 Subject: [PATCH 037/185] Add a `docker-plan` step to consolidate push and tag logic (#14083) The dist plan parsing is pretty hard to understand, and I want to add more images, e.g., for DockerHub in #14088. As a simplifying precursor... move the dist plan processing into a dedicated step. --- .github/workflows/build-docker.yml | 55 +++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 13 deletions(-) diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml index ce12d3c5d..e310add68 100644 --- a/.github/workflows/build-docker.yml +++ b/.github/workflows/build-docker.yml @@ -40,9 +40,35 @@ env: UV_BASE_IMG: ghcr.io/${{ github.repository_owner }}/uv jobs: + docker-plan: + name: plan + runs-on: ubuntu-latest + outputs: + push: ${{ steps.plan.outputs.push }} + tag: ${{ steps.plan.outputs.tag }} + action: ${{ steps.plan.outputs.action }} + steps: + - name: Set push variable + env: + DRY_RUN: ${{ inputs.plan == '' || fromJson(inputs.plan).announcement_tag_is_implicit }} + TAG: ${{ inputs.plan != '' && fromJson(inputs.plan).announcement_tag }} + id: plan + run: | + if [ "${{ env.DRY_RUN }}" == "false" ]; then + echo "push=true" >> "$GITHUB_OUTPUT" + echo "tag=${{ env.TAG }}" >> "$GITHUB_OUTPUT" + echo "action=build and publish" >> "$GITHUB_OUTPUT" + else + echo "push=false" >> "$GITHUB_OUTPUT" + echo "tag=dry-run" >> "$GITHUB_OUTPUT" + echo "action=build" >> "$GITHUB_OUTPUT" + fi + docker-publish-base: if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }} - name: uv + name: ${{ needs.docker-plan.outputs.action }} uv + needs: + - docker-plan runs-on: ubuntu-latest permissions: contents: read @@ -75,12 +101,12 @@ jobs: - uses: depot/setup-action@b0b1ea4f69e92ebf5dea3f8713a1b0c37b2126a5 - name: Check tag consistency - if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }} + if: ${{ needs.docker-plan.outputs.push == 'true' }} run: | version=$(grep "version = " pyproject.toml | sed -e 's/version = "\(.*\)"/\1/g') - if [ "${{ fromJson(inputs.plan).announcement_tag }}" != "${version}" ]; then + if [ "${{ needs.docker-plan.outputs.tag }}" != "${version}" ]; then echo "The input tag does not match the version from pyproject.toml:" >&2 - echo "${{ fromJson(inputs.plan).announcement_tag }}" >&2 + echo "${{ needs.docker-plan.outputs.tag }}" >&2 echo "${version}" >&2 exit 1 else @@ -94,9 +120,9 @@ jobs: images: ${{ env.UV_BASE_IMG }} # Defining this makes sure the org.opencontainers.image.version OCI label becomes the actual release version and not the branch name tags: | - type=raw,value=dry-run,enable=${{ inputs.plan == '' || fromJson(inputs.plan).announcement_tag_is_implicit }} - type=pep440,pattern={{ version }},value=${{ inputs.plan != '' && fromJson(inputs.plan).announcement_tag || 'dry-run' }},enable=${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }} - type=pep440,pattern={{ major }}.{{ minor }},value=${{ inputs.plan != '' && fromJson(inputs.plan).announcement_tag || 'dry-run' }},enable=${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }} + type=raw,value=dry-run,enable=${{ needs.docker-plan.outputs.push == 'false' }} + type=pep440,pattern={{ version }},value=${{ needs.docker-plan.outputs.tag }},enable=${{ needs.docker-plan.outputs.push }} + type=pep440,pattern={{ major }}.{{ minor }},value=${{ needs.docker-plan.outputs.tag }},enable=${{ needs.docker-plan.outputs.push }} - name: Build and push by digest id: build @@ -105,25 +131,26 @@ jobs: project: 7hd4vdzmw5 # astral-sh/uv context: . platforms: linux/amd64,linux/arm64 - push: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }} + push: ${{ needs.docker-plan.outputs.push }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - name: Generate artifact attestation for base image - if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }} + if: ${{ needs.docker-plan.outputs.push == 'true' }} uses: actions/attest-build-provenance@c074443f1aee8d4aeeae555aebba3282517141b2 # v2.2.3 with: subject-name: ${{ env.UV_BASE_IMG }} subject-digest: ${{ steps.build.outputs.digest }} docker-publish-extra: - name: ${{ matrix.image-mapping }} + name: ${{ needs.docker-plan.outputs.action }} ${{ matrix.image-mapping }} runs-on: ubuntu-latest environment: name: release needs: + - docker-plan - docker-publish-base - if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }} + if: ${{ needs.docker-plan.outputs.push == 'true' }} permissions: packages: write attestations: write # needed to push image attestations to the Github attestation store @@ -196,8 +223,8 @@ jobs: # Loop through all base tags and append its docker metadata pattern to the list # Order is on purpose such that the label org.opencontainers.image.version has the first pattern with the full version IFS=','; for TAG in ${BASE_TAGS}; do - TAG_PATTERNS="${TAG_PATTERNS}type=pep440,pattern={{ version }},suffix=-${TAG},value=${{ fromJson(inputs.plan).announcement_tag }}\n" - TAG_PATTERNS="${TAG_PATTERNS}type=pep440,pattern={{ major }}.{{ minor }},suffix=-${TAG},value=${{ fromJson(inputs.plan).announcement_tag }}\n" + TAG_PATTERNS="${TAG_PATTERNS}type=pep440,pattern={{ version }},suffix=-${TAG},value=${{ needs.docker-plan.outputs.tag }}\n" + TAG_PATTERNS="${TAG_PATTERNS}type=pep440,pattern={{ major }}.{{ minor }},suffix=-${TAG},value=${{ needs.docker-plan.outputs.tag }}\n" TAG_PATTERNS="${TAG_PATTERNS}type=raw,value=${TAG}\n" done @@ -249,8 +276,10 @@ jobs: environment: name: release needs: + - docker-plan - docker-publish-base - docker-publish-extra + if: ${{ needs.docker-plan.outputs.push == 'true' }} steps: - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 with: From 6c096246d826f68ba9a7f0e8cb6efa73b19aa24c Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 17 Jun 2025 16:49:00 -0400 Subject: [PATCH 038/185] Remove preview label from `--torch-backend` (#14119) This is now used in enough places that I'm comfortable committing to maintaining it under our versioning policy. Closes #14091. --- crates/uv/src/commands/pip/compile.rs | 26 ++++++++++++-------------- crates/uv/src/commands/pip/install.rs | 26 ++++++++++++-------------- crates/uv/src/commands/pip/sync.rs | 26 ++++++++++++-------------- docs/guides/integration/pytorch.md | 25 ++++++++++++++++--------- 4 files changed, 52 insertions(+), 51 deletions(-) diff --git a/crates/uv/src/commands/pip/compile.rs b/crates/uv/src/commands/pip/compile.rs index 20a60416f..197455aae 100644 --- a/crates/uv/src/commands/pip/compile.rs +++ b/crates/uv/src/commands/pip/compile.rs @@ -388,20 +388,18 @@ pub(crate) async fn pip_compile( } // Determine the PyTorch backend. - let torch_backend = torch_backend.map(|mode| { - if preview.is_disabled() { - warn_user!("The `--torch-backend` setting is experimental and may change without warning. Pass `--preview` to disable this warning."); - } - - TorchStrategy::from_mode( - mode, - python_platform - .map(TargetTriple::platform) - .as_ref() - .unwrap_or(interpreter.platform()) - .os(), - ) - }).transpose()?; + let torch_backend = torch_backend + .map(|mode| { + TorchStrategy::from_mode( + mode, + python_platform + .map(TargetTriple::platform) + .as_ref() + .unwrap_or(interpreter.platform()) + .os(), + ) + }) + .transpose()?; // Initialize the registry client. let client = RegistryClientBuilder::try_from(client_builder)? diff --git a/crates/uv/src/commands/pip/install.rs b/crates/uv/src/commands/pip/install.rs index 5fc9a66f4..e4f524c57 100644 --- a/crates/uv/src/commands/pip/install.rs +++ b/crates/uv/src/commands/pip/install.rs @@ -344,20 +344,18 @@ pub(crate) async fn pip_install( } // Determine the PyTorch backend. - let torch_backend = torch_backend.map(|mode| { - if preview.is_disabled() { - warn_user!("The `--torch-backend` setting is experimental and may change without warning. Pass `--preview` to disable this warning."); - } - - TorchStrategy::from_mode( - mode, - python_platform - .map(TargetTriple::platform) - .as_ref() - .unwrap_or(interpreter.platform()) - .os(), - ) - }).transpose()?; + let torch_backend = torch_backend + .map(|mode| { + TorchStrategy::from_mode( + mode, + python_platform + .map(TargetTriple::platform) + .as_ref() + .unwrap_or(interpreter.platform()) + .os(), + ) + }) + .transpose()?; // Initialize the registry client. let client = RegistryClientBuilder::try_from(client_builder)? diff --git a/crates/uv/src/commands/pip/sync.rs b/crates/uv/src/commands/pip/sync.rs index 35cef5907..e5bf92ae4 100644 --- a/crates/uv/src/commands/pip/sync.rs +++ b/crates/uv/src/commands/pip/sync.rs @@ -277,20 +277,18 @@ pub(crate) async fn pip_sync( } // Determine the PyTorch backend. - let torch_backend = torch_backend.map(|mode| { - if preview.is_disabled() { - warn_user!("The `--torch-backend` setting is experimental and may change without warning. Pass `--preview` to disable this warning."); - } - - TorchStrategy::from_mode( - mode, - python_platform - .map(TargetTriple::platform) - .as_ref() - .unwrap_or(interpreter.platform()) - .os(), - ) - }).transpose()?; + let torch_backend = torch_backend + .map(|mode| { + TorchStrategy::from_mode( + mode, + python_platform + .map(TargetTriple::platform) + .as_ref() + .unwrap_or(interpreter.platform()) + .os(), + ) + }) + .transpose()?; // Initialize the registry client. let client = RegistryClientBuilder::try_from(client_builder)? diff --git a/docs/guides/integration/pytorch.md b/docs/guides/integration/pytorch.md index 7fd0f8004..fb1d82b31 100644 --- a/docs/guides/integration/pytorch.md +++ b/docs/guides/integration/pytorch.md @@ -433,11 +433,14 @@ $ uv pip install torch torchvision torchaudio --index-url https://download.pytor ## Automatic backend selection -In [preview](../../reference/settings.md#preview), uv can automatically select the appropriate -PyTorch index at runtime by inspecting the system configuration via `--torch-backend=auto` (or -`UV_TORCH_BACKEND=auto`): +uv supports automatic selection of the appropriate PyTorch index via the `--torch-backend=auto` +command-line argument (or the `UV_TORCH_BACKEND=auto` environment variable), as in: ```shell +$ # With a command-line argument. +$ uv pip install torch --torch-backend=auto + +$ # With an environment variable. $ UV_TORCH_BACKEND=auto uv pip install torch ``` @@ -446,12 +449,16 @@ PyTorch index for all relevant packages (e.g., `torch`, `torchvision`, etc.). If is found, uv will fall back to the CPU-only index. uv will continue to respect existing index configuration for any packages outside the PyTorch ecosystem. -To select a specific backend (e.g., `cu126`), set `--torch-backend=cu126` (or -`UV_TORCH_BACKEND=cu126`). +You can also select a specific backend (e.g., CUDA 12.6) with `--torch-backend=cu126` (or +`UV_TORCH_BACKEND=cu126`): + +```shell +$ # With a command-line argument. +$ uv pip install torch torchvision --torch-backend=cu126 + +$ # With an environment variable. +$ UV_TORCH_BACKEND=cu126 uv pip install torch torchvision +``` At present, `--torch-backend` is only available in the `uv pip` interface, and only supports detection of CUDA drivers (as opposed to other accelerators like ROCm or Intel GPUs). - -As `--torch-backend` is a preview feature, it should be considered experimental and is not governed -by uv's standard [versioning policy](../../reference/policies/versioning.md). `--torch-backend` may -change or be removed entirely in future versions of uv. From 47c522f9be7ea61884c424d73bcd1d75cea77694 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Tue, 17 Jun 2025 17:45:11 -0500 Subject: [PATCH 039/185] Serialize Python requests for tools as canonicalized strings (#14109) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When working on support for reading global Python pins in tool operations, I noticed that we weren't using the canonicalized Python request in receipts — we were using the raw string provided by the user. Since we'll need to compare these values, we should be using the canonicalized string. The `Tool` and `ToolReceipt` types have been updated to hold a `PythonRequest` instead of a `String`, and `Serialize` was implemented for `PythonRequest` so canonicalization can happen at the edge instead of being the caller's responsibility. --- crates/uv-python/src/discovery.rs | 20 ++++++++++++++++++++ crates/uv-tool/src/tool.rs | 17 ++++++++++++----- crates/uv/src/commands/tool/common.rs | 10 +++++++--- crates/uv/src/commands/tool/install.rs | 8 +++++--- crates/uv/src/commands/tool/upgrade.rs | 4 ++-- 5 files changed, 46 insertions(+), 13 deletions(-) diff --git a/crates/uv-python/src/discovery.rs b/crates/uv-python/src/discovery.rs index 3858fd525..27853e3db 100644 --- a/crates/uv-python/src/discovery.rs +++ b/crates/uv-python/src/discovery.rs @@ -67,6 +67,26 @@ pub enum PythonRequest { Key(PythonDownloadRequest), } +impl<'a> serde::Deserialize<'a> for PythonRequest { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'a>, + { + let s = String::deserialize(deserializer)?; + Ok(PythonRequest::parse(&s)) + } +} + +impl serde::Serialize for PythonRequest { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let s = self.to_canonical_string(); + serializer.serialize_str(&s) + } +} + #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Deserialize)] #[serde(deny_unknown_fields, rename_all = "kebab-case")] #[cfg_attr(feature = "clap", derive(clap::ValueEnum))] diff --git a/crates/uv-tool/src/tool.rs b/crates/uv-tool/src/tool.rs index df8571c94..cce3a2f58 100644 --- a/crates/uv-tool/src/tool.rs +++ b/crates/uv-tool/src/tool.rs @@ -7,6 +7,7 @@ use toml_edit::{Array, Item, Table, Value, value}; use uv_distribution_types::Requirement; use uv_fs::{PortablePath, Simplified}; use uv_pypi_types::VerbatimParsedUrl; +use uv_python::PythonRequest; use uv_settings::ToolOptions; /// A tool entry. @@ -22,7 +23,7 @@ pub struct Tool { /// The build constraints requested by the user during installation. build_constraints: Vec, /// The Python requested by the user during installation. - python: Option, + python: Option, /// A mapping of entry point names to their metadata. entrypoints: Vec, /// The [`ToolOptions`] used to install this tool. @@ -40,7 +41,7 @@ struct ToolWire { overrides: Vec, #[serde(default)] build_constraint_dependencies: Vec, - python: Option, + python: Option, entrypoints: Vec, #[serde(default)] options: ToolOptions, @@ -164,7 +165,7 @@ impl Tool { constraints: Vec, overrides: Vec, build_constraints: Vec, - python: Option, + python: Option, entrypoints: impl Iterator, options: ToolOptions, ) -> Self { @@ -280,7 +281,13 @@ impl Tool { } if let Some(ref python) = self.python { - table.insert("python", value(python)); + table.insert( + "python", + value(serde::Serialize::serialize( + &python, + toml_edit::ser::ValueSerializer::new(), + )?), + ); } table.insert("entrypoints", { @@ -327,7 +334,7 @@ impl Tool { &self.build_constraints } - pub fn python(&self) -> &Option { + pub fn python(&self) -> &Option { &self.python } diff --git a/crates/uv/src/commands/tool/common.rs b/crates/uv/src/commands/tool/common.rs index 77aba8619..807225cbc 100644 --- a/crates/uv/src/commands/tool/common.rs +++ b/crates/uv/src/commands/tool/common.rs @@ -158,14 +158,18 @@ pub(crate) async fn refine_interpreter( Ok(Some(interpreter)) } -/// Installs tool executables for a given package and handles any conflicts. -pub(crate) fn install_executables( +/// Finalizes a tool installation, after creation of an environment. +/// +/// Installs tool executables for a given package, handling any conflicts. +/// +/// Adds a receipt for the tool. +pub(crate) fn finalize_tool_install( environment: &PythonEnvironment, name: &PackageName, installed_tools: &InstalledTools, options: ToolOptions, force: bool, - python: Option, + python: Option, requirements: Vec, constraints: Vec, overrides: Vec, diff --git a/crates/uv/src/commands/tool/install.rs b/crates/uv/src/commands/tool/install.rs index 86b0d4bc6..a65ad3af2 100644 --- a/crates/uv/src/commands/tool/install.rs +++ b/crates/uv/src/commands/tool/install.rs @@ -33,7 +33,9 @@ use crate::commands::project::{ EnvironmentSpecification, PlatformState, ProjectError, resolve_environment, resolve_names, sync_environment, update_environment, }; -use crate::commands::tool::common::{install_executables, refine_interpreter, remove_entrypoints}; +use crate::commands::tool::common::{ + finalize_tool_install, refine_interpreter, remove_entrypoints, +}; use crate::commands::tool::{Target, ToolRequest}; use crate::commands::{diagnostics, reporters::PythonDownloadReporter}; use crate::printer::Printer; @@ -592,13 +594,13 @@ pub(crate) async fn install( } }; - install_executables( + finalize_tool_install( &environment, &from.name, &installed_tools, options, force || invalid_tool_receipt, - python, + python_request, requirements, constraints, overrides, diff --git a/crates/uv/src/commands/tool/upgrade.rs b/crates/uv/src/commands/tool/upgrade.rs index 9f4d3bcab..c930ecada 100644 --- a/crates/uv/src/commands/tool/upgrade.rs +++ b/crates/uv/src/commands/tool/upgrade.rs @@ -29,7 +29,7 @@ use crate::commands::project::{ }; use crate::commands::reporters::PythonDownloadReporter; use crate::commands::tool::common::remove_entrypoints; -use crate::commands::{ExitStatus, conjunction, tool::common::install_executables}; +use crate::commands::{ExitStatus, conjunction, tool::common::finalize_tool_install}; use crate::printer::Printer; use crate::settings::{NetworkSettings, ResolverInstallerSettings}; @@ -375,7 +375,7 @@ async fn upgrade_tool( remove_entrypoints(&existing_tool_receipt); // If we modified the target tool, reinstall the entrypoints. - install_executables( + finalize_tool_install( &environment, name, installed_tools, From 2fc922144a35906e9744479d606f5b4b7b51802e Mon Sep 17 00:00:00 2001 From: John Mumm Date: Wed, 18 Jun 2025 03:43:45 -0400 Subject: [PATCH 040/185] Add script for testing uv against different registries (#13615) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR provides a script that uses environment variables to determine which registries to test. This script is being used to run automated registry tests in CI for AWS, Azure, GCP, Artifactory, GitLab, Cloudsmith, and Gemfury. You must configure the following required env vars for each registry: ``` UV_TEST__URL URL for the registry UV_TEST__TOKEN authentication token UV_TEST__PKG private package to install ``` The username defaults to "\_\_token\_\_" but can be optionally set with: ``` UV_TEST__USERNAME ``` For each configured registry, the test will attempt to install the specified package. Some registries can fall back to PyPI internally, so it's important to choose a package that only exists in the registry you are testing. Currently, a successful test means that it finds the line “ + ” in the output. This is because in its current form we don’t know ahead of time what package it is and hence what the exact expected output would be. The advantage if that anyone can run this locally, though they would have to have access to the registries they want to test. You can also use the `--use-op` command line argument to derive these test env vars from a 1Password vault (default is "RegistryTests" but can be configured with `--op-vault`). It will look at all items in the vault with names following the pattern `UV_TEST_` and will derive the env vars as follows: ``` `UV_TEST__USERNAME` from the `username` field `UV_TEST__TOKEN` from the `password` field `UV_TEST__URL` from a field with the label `url` `UV_TEST__PKG` from a field with the label `pkg` ``` --- .github/workflows/ci.yml | 84 ++++++++ scripts/registries-test.py | 422 +++++++++++++++++++++++++++++++++++++ 2 files changed, 506 insertions(+) create mode 100644 scripts/registries-test.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8a1f78c9e..feaa38210 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1416,6 +1416,90 @@ jobs: done <<< "${CHANGED_FILES}" echo "code_any_changed=${CODE_CHANGED}" >> "${GITHUB_OUTPUT}" + integration-test-registries: + timeout-minutes: 10 + needs: build-binary-linux-libc + name: "integration test | registries" + runs-on: ubuntu-latest + if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && github.event.pull_request.head.repo.fork != true }} + environment: uv-test-registries + env: + PYTHON_VERSION: 3.12 + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 + + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: "${{ env.PYTHON_VERSION }}" + + - name: "Download binary" + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: uv-linux-libc-${{ github.sha }} + + - name: "Prepare binary" + run: chmod +x ./uv + + - name: "Configure AWS credentials" + uses: aws-actions/configure-aws-credentials@b47578312673ae6fa5b5096b330d9fbac3d116df + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: us-east-1 + + - name: "Get AWS CodeArtifact token" + run: | + UV_TEST_AWS_TOKEN=$(aws codeartifact get-authorization-token \ + --domain tests \ + --domain-owner ${{ secrets.AWS_ACCOUNT_ID }} \ + --region us-east-1 \ + --query authorizationToken \ + --output text) + echo "::add-mask::$UV_TEST_AWS_TOKEN" + echo "UV_TEST_AWS_TOKEN=$UV_TEST_AWS_TOKEN" >> $GITHUB_ENV + + - name: "Authenticate with GCP" + id: "auth" + uses: "google-github-actions/auth@ba79af03959ebeac9769e648f473a284504d9193" + with: + credentials_json: "${{ secrets.GCP_SERVICE_ACCOUNT_KEY }}" + + - name: "Set up GCP SDK" + uses: "google-github-actions/setup-gcloud@77e7a554d41e2ee56fc945c52dfd3f33d12def9a" + + - name: "Get GCP Artifact Registry token" + id: get_token + run: | + UV_TEST_GCP_TOKEN=$(gcloud auth print-access-token) + echo "::add-mask::$UV_TEST_GCP_TOKEN" + echo "UV_TEST_GCP_TOKEN=$UV_TEST_GCP_TOKEN" >> $GITHUB_ENV + + - name: "Run registry tests" + run: ./uv run -p ${{ env.PYTHON_VERSION }} scripts/registries-test.py --uv ./uv --color always --all + env: + RUST_LOG: uv=debug + UV_TEST_ARTIFACTORY_TOKEN: ${{ secrets.UV_TEST_ARTIFACTORY_TOKEN }} + UV_TEST_ARTIFACTORY_URL: ${{ secrets.UV_TEST_ARTIFACTORY_URL }} + UV_TEST_ARTIFACTORY_USERNAME: ${{ secrets.UV_TEST_ARTIFACTORY_USERNAME }} + UV_TEST_AWS_URL: ${{ secrets.UV_TEST_AWS_URL }} + UV_TEST_AWS_USERNAME: aws + UV_TEST_AZURE_TOKEN: ${{ secrets.UV_TEST_AZURE_TOKEN }} + UV_TEST_AZURE_URL: ${{ secrets.UV_TEST_AZURE_URL }} + UV_TEST_AZURE_USERNAME: dummy + UV_TEST_CLOUDSMITH_TOKEN: ${{ secrets.UV_TEST_CLOUDSMITH_TOKEN }} + UV_TEST_CLOUDSMITH_URL: ${{ secrets.UV_TEST_CLOUDSMITH_URL }} + UV_TEST_CLOUDSMITH_USERNAME: ${{ secrets.UV_TEST_CLOUDSMITH_USERNAME }} + UV_TEST_GCP_URL: ${{ secrets.UV_TEST_GCP_URL }} + UV_TEST_GCP_USERNAME: oauth2accesstoken + UV_TEST_GEMFURY_TOKEN: ${{ secrets.UV_TEST_GEMFURY_TOKEN }} + UV_TEST_GEMFURY_URL: ${{ secrets.UV_TEST_GEMFURY_URL }} + UV_TEST_GEMFURY_USERNAME: ${{ secrets.UV_TEST_GEMFURY_USERNAME }} + UV_TEST_GITLAB_TOKEN: ${{ secrets.UV_TEST_GITLAB_TOKEN }} + UV_TEST_GITLAB_URL: ${{ secrets.UV_TEST_GITLAB_URL }} + UV_TEST_GITLAB_USERNAME: token + integration-test-publish: timeout-minutes: 20 needs: integration-test-publish-changed diff --git a/scripts/registries-test.py b/scripts/registries-test.py new file mode 100644 index 000000000..2d4c1d2aa --- /dev/null +++ b/scripts/registries-test.py @@ -0,0 +1,422 @@ +#!/usr/bin/env python3 +""" +Test `uv add` against multiple Python package registries. + +This script looks for environment variables that configure registries for testing. +To configure a registry, set the following environment variables: + + `UV_TEST__URL` URL for the registry + `UV_TEST__TOKEN` authentication token + +The username defaults to "__token__" but can be optionally set with: + `UV_TEST__USERNAME` + +The package to install defaults to "astral-registries-test-pkg" but can be optionally +set with: + `UV_TEST__PKG` + +Keep in mind that some registries can fall back to PyPI internally, so make sure +you choose a package that only exists in the registry you are testing. + +You can also use the 1Password CLI to fetch registry credentials from a vault by passing +the `--use-op` flag. For each item in the vault named `UV_TEST_XXX`, the script will set +env vars for any of the following fields, if present: + `UV_TEST__USERNAME` from the `username` field + `UV_TEST__TOKEN` from the `password` field + `UV_TEST__URL` from a field with the label `url` + `UV_TEST__PKG` from a field with the label `pkg` + +# /// script +# requires-python = ">=3.12" +# dependencies = ["colorama>=0.4.6"] +# /// +""" + +import argparse +import json +import os +import re +import subprocess +import sys +import tempfile +from pathlib import Path +from typing import Dict + +import colorama +from colorama import Fore + + +def initialize_colorama(force_color=False): + colorama.init(strip=not force_color, autoreset=True) + + +cwd = Path(__file__).parent + +DEFAULT_TIMEOUT = 30 +DEFAULT_PKG_NAME = "astral-registries-test-pkg" + +KNOWN_REGISTRIES = [ + "artifactory", + "azure", + "aws", + "cloudsmith", + "gcp", + "gemfury", + "gitlab", +] + + +def fetch_op_items(vault_name: str, env: Dict[str, str]) -> Dict[str, str]: + """Fetch items from the specified 1Password vault and add them to the environment. + + For each item named UV_TEST_XXX in the vault: + - Set `UV_TEST_XXX_USERNAME` to the `username` field + - Set `UV_TEST_XXX_TOKEN` to the `password` field + - Set `UV_TEST_XXX_URL` to the `url` field + + Raises exceptions for any 1Password CLI errors so they can be handled by the caller. + """ + # Run 'op item list' to get all items in the vault + result = subprocess.run( + ["op", "item", "list", "--vault", vault_name, "--format", "json"], + capture_output=True, + text=True, + check=True, + ) + + items = json.loads(result.stdout) + updated_env = env.copy() + + for item in items: + item_id = item["id"] + item_title = item["title"] + + # Only process items that match the registry naming pattern + if item_title.startswith("UV_TEST_"): + # Extract the registry name (e.g., "AWS" from "UV_TEST_AWS") + registry_name = item_title.removeprefix("UV_TEST_") + + # Get the item details + item_details = subprocess.run( + ["op", "item", "get", item_id, "--format", "json"], + capture_output=True, + text=True, + check=True, + ) + + item_data = json.loads(item_details.stdout) + + username = None + password = None + url = None + pkg = None + + if "fields" in item_data: + for field in item_data["fields"]: + if field.get("id") == "username": + username = field.get("value") + elif field.get("id") == "password": + password = field.get("value") + elif field.get("label") == "url": + url = field.get("value") + elif field.get("label") == "pkg": + pkg = field.get("value") + if username: + updated_env[f"UV_TEST_{registry_name}_USERNAME"] = username + if password: + updated_env[f"UV_TEST_{registry_name}_TOKEN"] = password + if url: + updated_env[f"UV_TEST_{registry_name}_URL"] = url + if pkg: + updated_env[f"UV_TEST_{registry_name}_PKG"] = pkg + + print(f"Added 1Password credentials for {registry_name}") + + return updated_env + + +def get_registries(env: Dict[str, str]) -> Dict[str, str]: + pattern = re.compile(r"^UV_TEST_(.+)_URL$") + registries: Dict[str, str] = {} + + for env_var, value in env.items(): + match = pattern.match(env_var) + if match: + registry_name = match.group(1).lower() + registries[registry_name] = value + + return registries + + +def setup_test_project( + registry_name: str, registry_url: str, project_dir: str, requires_python: str +): + """Create a temporary project directory with a pyproject.toml""" + pyproject_content = f"""[project] +name = "{registry_name}-test" +version = "0.1.0" +description = "Test registry" +requires-python = ">={requires_python}" + +[[tool.uv.index]] +name = "{registry_name}" +url = "{registry_url}" +default = true +""" + pyproject_file = Path(project_dir) / "pyproject.toml" + pyproject_file.write_text(pyproject_content) + + +def run_test( + env: dict[str, str], + uv: Path, + registry_name: str, + registry_url: str, + package: str, + username: str, + token: str, + verbosity: int, + timeout: int, + requires_python: str, +) -> bool: + print(uv) + """Attempt to install a package from this registry.""" + print( + f"{registry_name} -- Running test for {registry_url} with username {username}" + ) + if package == DEFAULT_PKG_NAME: + print( + f"** Using default test package name: {package}. To choose a different package, set UV_TEST_{registry_name.upper()}_PKG" + ) + print(f"\nAttempting to install {package}") + env[f"UV_INDEX_{registry_name.upper()}_USERNAME"] = username + env[f"UV_INDEX_{registry_name.upper()}_PASSWORD"] = token + + with tempfile.TemporaryDirectory() as project_dir: + setup_test_project(registry_name, registry_url, project_dir, requires_python) + + cmd = [ + uv, + "add", + package, + "--directory", + project_dir, + ] + if verbosity: + cmd.extend(["-" + "v" * verbosity]) + + result = None + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + timeout=timeout, + check=False, + env=env, + ) + + if result.returncode != 0: + error_msg = result.stderr.strip() if result.stderr else "Unknown error" + print(f"{Fore.RED}{registry_name}: FAIL{Fore.RESET} \n\n{error_msg}") + return False + + success = False + for line in result.stderr.strip().split("\n"): + if line.startswith(f" + {package}=="): + success = True + if success: + print(f"{Fore.GREEN}{registry_name}: PASS") + if verbosity > 0: + print(f" stdout: {result.stdout.strip()}") + print(f" stderr: {result.stderr.strip()}") + return True + else: + print( + f"{Fore.RED}{registry_name}: FAIL{Fore.RESET} - Failed to install {package}." + ) + + except subprocess.TimeoutExpired: + print(f"{Fore.RED}{registry_name}: TIMEOUT{Fore.RESET} (>{timeout}s)") + except FileNotFoundError: + print(f"{Fore.RED}{registry_name}: ERROR{Fore.RESET} - uv not found") + except Exception as e: + print(f"{Fore.RED}{registry_name}: ERROR{Fore.RESET} - {e}") + + if result: + if result.stdout: + print(f"{Fore.RED} stdout:{Fore.RESET} {result.stdout.strip()}") + if result.stderr: + print(f"\n{Fore.RED} stderr:{Fore.RESET} {result.stderr.strip()}") + return False + + +def parse_args() -> argparse.Namespace: + """Parse command line arguments""" + parser = argparse.ArgumentParser( + description="Test uv add command against multiple registries", + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument( + "--all", + action="store_true", + help="fail if any known registry was not tested", + ) + parser.add_argument( + "--uv", + type=str, + help="specify a path to the uv binary (default: uv command)", + ) + parser.add_argument( + "--timeout", + type=int, + default=os.environ.get("UV_TEST_TIMEOUT", DEFAULT_TIMEOUT), + help=f"timeout in seconds for each test (default: {DEFAULT_TIMEOUT} or UV_TEST_TIMEOUT)", + ) + parser.add_argument( + "-v", + "--verbose", + action="count", + default=0, + help="increase verbosity (-v for debug, -vv for trace)", + ) + parser.add_argument( + "--use-op", + action="store_true", + help="use 1Password CLI to fetch registry credentials from the specified vault", + ) + parser.add_argument( + "--op-vault", + type=str, + default="RegistryTests", + help="name of the 1Password vault to use (default: RegistryTests)", + ) + parser.add_argument( + "--required-python", + type=str, + default="3.12", + help="minimum Python version for tests (default: 3.12)", + ) + parser.add_argument("--color", choices=["always", "auto", "never"], default="auto") + return parser.parse_args() + + +def main() -> None: + args = parse_args() + env = os.environ.copy() + + if args.color == "always": + initialize_colorama(force_color=True) + elif args.color == "never": + initialize_colorama(force_color=False) + else: + initialize_colorama(force_color=sys.stdout.isatty()) + + # If using 1Password, fetch credentials from the vault + if args.use_op: + print(f"Fetching credentials from 1Password vault '{args.op_vault}'...") + try: + env = fetch_op_items(args.op_vault, env) + except Exception as e: + print(f"{Fore.RED}Error accessing 1Password: {e}{Fore.RESET}") + print( + f"{Fore.YELLOW}Hint: If you're not authenticated, run 'op signin' first.{Fore.RESET}" + ) + sys.exit(1) + + if args.uv: + # We change the working directory for the subprocess calls, so we have to + # absolutize the path. + uv = Path.cwd().joinpath(args.uv) + else: + subprocess.run(["cargo", "build"]) + executable_suffix = ".exe" if os.name == "nt" else "" + uv = cwd.parent.joinpath(f"target/debug/uv{executable_suffix}") + + passed = [] + failed = [] + skipped = [] + untested_registries = set(KNOWN_REGISTRIES) + + print("Running tests...") + for registry_name, registry_url in get_registries(env).items(): + print("----------------") + + token = env.get(f"UV_TEST_{registry_name.upper()}_TOKEN") + if not token: + if args.all: + print( + f"{Fore.RED}{registry_name}: UV_TEST_{registry_name.upper()}_TOKEN contained no token. Required by --all" + ) + failed.append(registry_name) + else: + print( + f"{Fore.YELLOW}{registry_name}: UV_TEST_{registry_name.upper()}_TOKEN contained no token. Skipping test" + ) + skipped.append(registry_name) + continue + + # The private package we will test installing + package = env.get(f"UV_TEST_{registry_name.upper()}_PKG", DEFAULT_PKG_NAME) + username = env.get(f"UV_TEST_{registry_name.upper()}_USERNAME", "__token__") + + if run_test( + env, + uv, + registry_name, + registry_url, + package, + username, + token, + args.verbose, + args.timeout, + args.required_python, + ): + passed.append(registry_name) + else: + failed.append(registry_name) + + untested_registries.remove(registry_name) + + total = len(passed) + len(failed) + + print("----------------") + if passed: + print(f"\n{Fore.GREEN}Passed:") + for registry_name in passed: + print(f" * {registry_name}") + if failed: + print(f"\n{Fore.RED}Failed:") + for registry_name in failed: + print(f" * {registry_name}") + if skipped: + print(f"\n{Fore.YELLOW}Skipped:") + for registry_name in skipped: + print(f" * {registry_name}") + + print(f"\nResults: {len(passed)}/{total} tests passed, {len(skipped)} skipped") + + if args.all and len(untested_registries) > 0: + print( + f"\n{Fore.RED}Failed to test all known registries (requested via --all).{Fore.RESET}\nMissing:" + ) + for registry_name in untested_registries: + print(f" * {registry_name}") + print("You must use the exact registry name as listed here") + sys.exit(1) + + if total == 0: + print("\nNo tests were run - have you defined at least one registry?") + print(" * UV_TEST__URL") + print(" * UV_TEST__TOKEN") + print( + " * UV_TEST__PKG (the private package to test installing)" + ) + print(' * UV_TEST__USERNAME (defaults to "__token__")') + sys.exit(1) + + sys.exit(0 if len(failed) == 0 else 1) + + +if __name__ == "__main__": + main() From 499c8aa808b0a245d498b7ce6fa6f1f68a99251f Mon Sep 17 00:00:00 2001 From: konsti Date: Wed, 18 Jun 2025 09:51:53 +0200 Subject: [PATCH 041/185] Fix PyPI publish test script (#14116) The script stumbled over a newline introduced in https://github.com/pypi/warehouse/pull/18266 (which is valid). Also fixed: Don't read versions for the same package from other indexes. We were using `project_name` here instead of `target`, while using the latter and only reading from a single index simplifies the code too. --- scripts/publish/test_publish.py | 58 ++++++++++++++++----------------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/scripts/publish/test_publish.py b/scripts/publish/test_publish.py index 620a08e61..c2c35fe90 100644 --- a/scripts/publish/test_publish.py +++ b/scripts/publish/test_publish.py @@ -163,39 +163,37 @@ all_targets: dict[str, TargetConfiguration] = local_targets | { } -def get_latest_version(project_name: str, client: httpx.Client) -> Version: +def get_latest_version(target: str, client: httpx.Client) -> Version: """Return the latest version on all indexes of the package.""" # To keep the number of packages small we reuse them across targets, so we have to # pick a version that doesn't exist on any target yet versions = set() - for target_config in all_targets.values(): - if target_config.project_name != project_name: - continue - url = target_config.index_url + project_name + "/" + target_config = all_targets[target] + url = target_config.index_url + target_config.project_name + "/" - # Get with retries - error = None - for _ in range(5): - try: - versions.update(collect_versions(url, client)) - break - except httpx.HTTPError as err: - error = err - print( - f"Error getting version for {project_name}, sleeping for 1s: {err}", - file=sys.stderr, - ) - time.sleep(1) - except InvalidSdistFilename as err: - # Sometimes there's a link that says "status page" - error = err - print( - f"Invalid index page for {project_name}, sleeping for 1s: {err}", - file=sys.stderr, - ) - time.sleep(1) - else: - raise RuntimeError(f"Failed to fetch {url}") from error + # Get with retries + error = None + for _ in range(5): + try: + versions.update(collect_versions(url, client)) + break + except httpx.HTTPError as err: + error = err + print( + f"Error getting version for {target_config.project_name}, sleeping for 1s: {err}", + file=sys.stderr, + ) + time.sleep(1) + except InvalidSdistFilename as err: + # Sometimes there's a link that says "status page" + error = err + print( + f"Invalid index page for {target_config.project_name}, sleeping for 1s: {err}", + file=sys.stderr, + ) + time.sleep(1) + else: + raise RuntimeError(f"Failed to fetch {url}") from error return max(versions) @@ -223,7 +221,7 @@ def get_filenames(url: str, client: httpx.Client) -> list[str]: response = client.get(url) data = response.text # Works for the indexes in the list - href_text = r"([^<>]+)" + href_text = r"([^<>]+)" return [m.group(1) for m in re.finditer(href_text, data)] @@ -363,7 +361,7 @@ def publish_project(target: str, uv: Path, client: httpx.Client): print(f"\nPublish {project_name} for {target}", file=sys.stderr) # The distributions are build to the dist directory of the project. - previous_version = get_latest_version(project_name, client) + previous_version = get_latest_version(target, client) version = get_new_version(previous_version) project_dir = build_project_at_version(target, version, uv) From 0cac73dc1f8d56e9a2440a28ab863d1dc7398c97 Mon Sep 17 00:00:00 2001 From: Aaron Ang <67321817+aaron-ang@users.noreply.github.com> Date: Wed, 18 Jun 2025 01:48:21 -0700 Subject: [PATCH 042/185] Warn on empty index directory (#13940) Close #13922 ## Summary Add a warning if the directory given by the `--index` argument is empty. ## Test Plan Added test case `add_index_empty_directory` in `edit.rs` --- crates/uv-distribution-types/src/index_url.rs | 39 ++++++++----------- crates/uv/src/commands/project/add.rs | 11 +++++- crates/uv/tests/it/edit.rs | 32 +++++++++++++++ 3 files changed, 57 insertions(+), 25 deletions(-) diff --git a/crates/uv-distribution-types/src/index_url.rs b/crates/uv-distribution-types/src/index_url.rs index 9604fbd30..a523b4811 100644 --- a/crates/uv-distribution-types/src/index_url.rs +++ b/crates/uv-distribution-types/src/index_url.rs @@ -511,30 +511,23 @@ impl<'a> IndexUrls { /// iterator. pub fn defined_indexes(&'a self) -> impl Iterator + 'a { if self.no_index { - Either::Left(std::iter::empty()) - } else { - Either::Right( - { - let mut seen = FxHashSet::default(); - self.indexes - .iter() - .filter(move |index| { - index.name.as_ref().is_none_or(|name| seen.insert(name)) - }) - .filter(|index| !index.default) - } - .chain({ - let mut seen = FxHashSet::default(); - self.indexes - .iter() - .filter(move |index| { - index.name.as_ref().is_none_or(|name| seen.insert(name)) - }) - .find(|index| index.default) - .into_iter() - }), - ) + return Either::Left(std::iter::empty()); } + + let mut seen = FxHashSet::default(); + let (non_default, default) = self + .indexes + .iter() + .filter(move |index| { + if let Some(name) = &index.name { + seen.insert(name) + } else { + true + } + }) + .partition::, _>(|index| !index.default); + + Either::Right(non_default.into_iter().chain(default)) } /// Return the `--no-index` flag. diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index 8e3c4a03a..1c5297f90 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -512,8 +512,9 @@ pub(crate) async fn add( )?; // Validate any indexes that were provided on the command-line to ensure - // they point to existing directories when using path URLs. - for index in &indexes { + // they point to existing non-empty directories when using path URLs. + let mut valid_indexes = Vec::with_capacity(indexes.len()); + for index in indexes { if let IndexUrl::Path(url) = &index.url { let path = url .to_file_path() @@ -521,8 +522,14 @@ pub(crate) async fn add( if !path.is_dir() { bail!("Directory not found for index: {url}"); } + if fs_err::read_dir(&path)?.next().is_none() { + warn_user_once!("Index directory `{url}` is empty, skipping"); + continue; + } } + valid_indexes.push(index); } + let indexes = valid_indexes; // Add any indexes that were provided on the command-line, in priority order. if !raw { diff --git a/crates/uv/tests/it/edit.rs b/crates/uv/tests/it/edit.rs index a66cd2ed5..68eae5110 100644 --- a/crates/uv/tests/it/edit.rs +++ b/crates/uv/tests/it/edit.rs @@ -9434,6 +9434,38 @@ fn add_index_with_non_existent_relative_path_with_same_name_as_index() -> Result Ok(()) } +#[test] +fn add_index_empty_directory() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! {r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + "#})?; + + let packages = context.temp_dir.child("test-index"); + packages.create_dir_all()?; + + uv_snapshot!(context.filters(), context.add().arg("iniconfig").arg("--index").arg("test-index"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: Index directory `file://[TEMP_DIR]/test-index` is empty, skipping + Resolved 2 packages in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + iniconfig==2.0.0 + "); + + Ok(()) +} + /// Add a PyPI requirement. #[test] fn add_group_comment() -> Result<()> { From 4d9c9a1e76bb53c6f04ca55b6032e393b1ac42bb Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 18 Jun 2025 07:35:05 -0400 Subject: [PATCH 043/185] Add ROCm backends to `--torch-backend` (#14120) We don't yet support automatic detection, but this at least allows explicit selection (e.g., `uv pip install --torch-backend rocm5.3`). Closes #14087. --- clippy.toml | 1 + crates/uv-resolver/src/resolver/system.rs | 16 +- crates/uv-torch/src/backend.rs | 226 +++++++++++++++++++++- docs/reference/cli.md | 48 +++++ uv.schema.json | 112 +++++++++++ 5 files changed, 397 insertions(+), 6 deletions(-) diff --git a/clippy.toml b/clippy.toml index 191195e33..bb1f365b5 100644 --- a/clippy.toml +++ b/clippy.toml @@ -6,6 +6,7 @@ doc-valid-idents = [ "GraalPy", "ReFS", "PyTorch", + "ROCm", ".." # Include the defaults ] diff --git a/crates/uv-resolver/src/resolver/system.rs b/crates/uv-resolver/src/resolver/system.rs index 806b1c01c..a47000846 100644 --- a/crates/uv-resolver/src/resolver/system.rs +++ b/crates/uv-resolver/src/resolver/system.rs @@ -23,11 +23,17 @@ impl SystemDependency { /// For example, given `https://download.pytorch.org/whl/cu124`, returns CUDA 12.4. pub(super) fn from_index(index: &DisplaySafeUrl) -> Option { let backend = TorchBackend::from_index(index)?; - let cuda_version = backend.cuda_version()?; - Some(Self { - name: PackageName::from_str("cuda").unwrap(), - version: cuda_version, - }) + if let Some(cuda_version) = backend.cuda_version() { + Some(Self { + name: PackageName::from_str("cuda").unwrap(), + version: cuda_version, + }) + } else { + backend.rocm_version().map(|rocm_version| Self { + name: PackageName::from_str("rocm").unwrap(), + version: rocm_version, + }) + } } } diff --git a/crates/uv-torch/src/backend.rs b/crates/uv-torch/src/backend.rs index 0df5bd844..263ea07bd 100644 --- a/crates/uv-torch/src/backend.rs +++ b/crates/uv-torch/src/backend.rs @@ -35,7 +35,6 @@ //! OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE //! OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. //! ``` -//! use std::str::FromStr; use std::sync::LazyLock; @@ -108,6 +107,70 @@ pub enum TorchMode { Cu90, /// Use the PyTorch index for CUDA 8.0. Cu80, + /// Use the PyTorch index for ROCm 6.3. + #[serde(rename = "rocm6.3")] + #[clap(name = "rocm6.3")] + Rocm63, + /// Use the PyTorch index for ROCm 6.2.4. + #[serde(rename = "rocm6.2.4")] + #[clap(name = "rocm6.2.4")] + Rocm624, + /// Use the PyTorch index for ROCm 6.2. + #[serde(rename = "rocm6.2")] + #[clap(name = "rocm6.2")] + Rocm62, + /// Use the PyTorch index for ROCm 6.1. + #[serde(rename = "rocm6.1")] + #[clap(name = "rocm6.1")] + Rocm61, + /// Use the PyTorch index for ROCm 6.0. + #[serde(rename = "rocm6.0")] + #[clap(name = "rocm6.0")] + Rocm60, + /// Use the PyTorch index for ROCm 5.7. + #[serde(rename = "rocm5.7")] + #[clap(name = "rocm5.7")] + Rocm57, + /// Use the PyTorch index for ROCm 5.6. + #[serde(rename = "rocm5.6")] + #[clap(name = "rocm5.6")] + Rocm56, + /// Use the PyTorch index for ROCm 5.5. + #[serde(rename = "rocm5.5")] + #[clap(name = "rocm5.5")] + Rocm55, + /// Use the PyTorch index for ROCm 5.4.2. + #[serde(rename = "rocm5.4.2")] + #[clap(name = "rocm5.4.2")] + Rocm542, + /// Use the PyTorch index for ROCm 5.4. + #[serde(rename = "rocm5.4")] + #[clap(name = "rocm5.4")] + Rocm54, + /// Use the PyTorch index for ROCm 5.3. + #[serde(rename = "rocm5.3")] + #[clap(name = "rocm5.3")] + Rocm53, + /// Use the PyTorch index for ROCm 5.2. + #[serde(rename = "rocm5.2")] + #[clap(name = "rocm5.2")] + Rocm52, + /// Use the PyTorch index for ROCm 5.1.1. + #[serde(rename = "rocm5.1.1")] + #[clap(name = "rocm5.1.1")] + Rocm511, + /// Use the PyTorch index for ROCm 4.2. + #[serde(rename = "rocm4.2")] + #[clap(name = "rocm4.2")] + Rocm42, + /// Use the PyTorch index for ROCm 4.1. + #[serde(rename = "rocm4.1")] + #[clap(name = "rocm4.1")] + Rocm41, + /// Use the PyTorch index for ROCm 4.0.1. + #[serde(rename = "rocm4.0.1")] + #[clap(name = "rocm4.0.1")] + Rocm401, } /// The strategy to use when determining the appropriate PyTorch index. @@ -158,6 +221,22 @@ impl TorchStrategy { TorchMode::Cu91 => Ok(Self::Backend(TorchBackend::Cu91)), TorchMode::Cu90 => Ok(Self::Backend(TorchBackend::Cu90)), TorchMode::Cu80 => Ok(Self::Backend(TorchBackend::Cu80)), + TorchMode::Rocm63 => Ok(Self::Backend(TorchBackend::Rocm63)), + TorchMode::Rocm624 => Ok(Self::Backend(TorchBackend::Rocm624)), + TorchMode::Rocm62 => Ok(Self::Backend(TorchBackend::Rocm62)), + TorchMode::Rocm61 => Ok(Self::Backend(TorchBackend::Rocm61)), + TorchMode::Rocm60 => Ok(Self::Backend(TorchBackend::Rocm60)), + TorchMode::Rocm57 => Ok(Self::Backend(TorchBackend::Rocm57)), + TorchMode::Rocm56 => Ok(Self::Backend(TorchBackend::Rocm56)), + TorchMode::Rocm55 => Ok(Self::Backend(TorchBackend::Rocm55)), + TorchMode::Rocm542 => Ok(Self::Backend(TorchBackend::Rocm542)), + TorchMode::Rocm54 => Ok(Self::Backend(TorchBackend::Rocm54)), + TorchMode::Rocm53 => Ok(Self::Backend(TorchBackend::Rocm53)), + TorchMode::Rocm52 => Ok(Self::Backend(TorchBackend::Rocm52)), + TorchMode::Rocm511 => Ok(Self::Backend(TorchBackend::Rocm511)), + TorchMode::Rocm42 => Ok(Self::Backend(TorchBackend::Rocm42)), + TorchMode::Rocm41 => Ok(Self::Backend(TorchBackend::Rocm41)), + TorchMode::Rocm401 => Ok(Self::Backend(TorchBackend::Rocm401)), } } @@ -177,6 +256,8 @@ impl TorchStrategy { | "torchtext" | "torchvision" | "pytorch-triton" + | "pytorch-triton-rocm" + | "pytorch-triton-xpu" ) } @@ -259,6 +340,22 @@ pub enum TorchBackend { Cu91, Cu90, Cu80, + Rocm63, + Rocm624, + Rocm62, + Rocm61, + Rocm60, + Rocm57, + Rocm56, + Rocm55, + Rocm542, + Rocm54, + Rocm53, + Rocm52, + Rocm511, + Rocm42, + Rocm41, + Rocm401, } impl TorchBackend { @@ -290,6 +387,22 @@ impl TorchBackend { Self::Cu91 => &CU91_INDEX_URL, Self::Cu90 => &CU90_INDEX_URL, Self::Cu80 => &CU80_INDEX_URL, + Self::Rocm63 => &ROCM63_INDEX_URL, + Self::Rocm624 => &ROCM624_INDEX_URL, + Self::Rocm62 => &ROCM62_INDEX_URL, + Self::Rocm61 => &ROCM61_INDEX_URL, + Self::Rocm60 => &ROCM60_INDEX_URL, + Self::Rocm57 => &ROCM57_INDEX_URL, + Self::Rocm56 => &ROCM56_INDEX_URL, + Self::Rocm55 => &ROCM55_INDEX_URL, + Self::Rocm542 => &ROCM542_INDEX_URL, + Self::Rocm54 => &ROCM54_INDEX_URL, + Self::Rocm53 => &ROCM53_INDEX_URL, + Self::Rocm52 => &ROCM52_INDEX_URL, + Self::Rocm511 => &ROCM511_INDEX_URL, + Self::Rocm42 => &ROCM42_INDEX_URL, + Self::Rocm41 => &ROCM41_INDEX_URL, + Self::Rocm401 => &ROCM401_INDEX_URL, } } @@ -336,6 +449,69 @@ impl TorchBackend { TorchBackend::Cu91 => Some(Version::new([9, 1])), TorchBackend::Cu90 => Some(Version::new([9, 0])), TorchBackend::Cu80 => Some(Version::new([8, 0])), + TorchBackend::Rocm63 => None, + TorchBackend::Rocm624 => None, + TorchBackend::Rocm62 => None, + TorchBackend::Rocm61 => None, + TorchBackend::Rocm60 => None, + TorchBackend::Rocm57 => None, + TorchBackend::Rocm56 => None, + TorchBackend::Rocm55 => None, + TorchBackend::Rocm542 => None, + TorchBackend::Rocm54 => None, + TorchBackend::Rocm53 => None, + TorchBackend::Rocm52 => None, + TorchBackend::Rocm511 => None, + TorchBackend::Rocm42 => None, + TorchBackend::Rocm41 => None, + TorchBackend::Rocm401 => None, + } + } + + /// Returns the ROCM [`Version`] for the given [`TorchBackend`]. + pub fn rocm_version(&self) -> Option { + match self { + TorchBackend::Cpu => None, + TorchBackend::Cu128 => None, + TorchBackend::Cu126 => None, + TorchBackend::Cu125 => None, + TorchBackend::Cu124 => None, + TorchBackend::Cu123 => None, + TorchBackend::Cu122 => None, + TorchBackend::Cu121 => None, + TorchBackend::Cu120 => None, + TorchBackend::Cu118 => None, + TorchBackend::Cu117 => None, + TorchBackend::Cu116 => None, + TorchBackend::Cu115 => None, + TorchBackend::Cu114 => None, + TorchBackend::Cu113 => None, + TorchBackend::Cu112 => None, + TorchBackend::Cu111 => None, + TorchBackend::Cu110 => None, + TorchBackend::Cu102 => None, + TorchBackend::Cu101 => None, + TorchBackend::Cu100 => None, + TorchBackend::Cu92 => None, + TorchBackend::Cu91 => None, + TorchBackend::Cu90 => None, + TorchBackend::Cu80 => None, + TorchBackend::Rocm63 => Some(Version::new([6, 3])), + TorchBackend::Rocm624 => Some(Version::new([6, 2, 4])), + TorchBackend::Rocm62 => Some(Version::new([6, 2])), + TorchBackend::Rocm61 => Some(Version::new([6, 1])), + TorchBackend::Rocm60 => Some(Version::new([6, 0])), + TorchBackend::Rocm57 => Some(Version::new([5, 7])), + TorchBackend::Rocm56 => Some(Version::new([5, 6])), + TorchBackend::Rocm55 => Some(Version::new([5, 5])), + TorchBackend::Rocm542 => Some(Version::new([5, 4, 2])), + TorchBackend::Rocm54 => Some(Version::new([5, 4])), + TorchBackend::Rocm53 => Some(Version::new([5, 3])), + TorchBackend::Rocm52 => Some(Version::new([5, 2])), + TorchBackend::Rocm511 => Some(Version::new([5, 1, 1])), + TorchBackend::Rocm42 => Some(Version::new([4, 2])), + TorchBackend::Rocm41 => Some(Version::new([4, 1])), + TorchBackend::Rocm401 => Some(Version::new([4, 0, 1])), } } } @@ -370,6 +546,22 @@ impl FromStr for TorchBackend { "cu91" => Ok(TorchBackend::Cu91), "cu90" => Ok(TorchBackend::Cu90), "cu80" => Ok(TorchBackend::Cu80), + "rocm6.3" => Ok(TorchBackend::Rocm63), + "rocm6.2.4" => Ok(TorchBackend::Rocm624), + "rocm6.2" => Ok(TorchBackend::Rocm62), + "rocm6.1" => Ok(TorchBackend::Rocm61), + "rocm6.0" => Ok(TorchBackend::Rocm60), + "rocm5.7" => Ok(TorchBackend::Rocm57), + "rocm5.6" => Ok(TorchBackend::Rocm56), + "rocm5.5" => Ok(TorchBackend::Rocm55), + "rocm5.4.2" => Ok(TorchBackend::Rocm542), + "rocm5.4" => Ok(TorchBackend::Rocm54), + "rocm5.3" => Ok(TorchBackend::Rocm53), + "rocm5.2" => Ok(TorchBackend::Rocm52), + "rocm5.1.1" => Ok(TorchBackend::Rocm511), + "rocm4.2" => Ok(TorchBackend::Rocm42), + "rocm4.1" => Ok(TorchBackend::Rocm41), + "rocm4.0.1" => Ok(TorchBackend::Rocm401), _ => Err(format!("Unknown PyTorch backend: {s}")), } } @@ -501,3 +693,35 @@ static CU90_INDEX_URL: LazyLock = LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/cu90").unwrap()); static CU80_INDEX_URL: LazyLock = LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/cu80").unwrap()); +static ROCM63_INDEX_URL: LazyLock = + LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/rocm6.3").unwrap()); +static ROCM624_INDEX_URL: LazyLock = + LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/rocm6.2.4").unwrap()); +static ROCM62_INDEX_URL: LazyLock = + LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/rocm6.2").unwrap()); +static ROCM61_INDEX_URL: LazyLock = + LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/rocm6.1").unwrap()); +static ROCM60_INDEX_URL: LazyLock = + LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/rocm6.0").unwrap()); +static ROCM57_INDEX_URL: LazyLock = + LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/rocm5.7").unwrap()); +static ROCM56_INDEX_URL: LazyLock = + LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/rocm5.6").unwrap()); +static ROCM55_INDEX_URL: LazyLock = + LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/rocm5.5").unwrap()); +static ROCM542_INDEX_URL: LazyLock = + LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/rocm5.4.2").unwrap()); +static ROCM54_INDEX_URL: LazyLock = + LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/rocm5.4").unwrap()); +static ROCM53_INDEX_URL: LazyLock = + LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/rocm5.3").unwrap()); +static ROCM52_INDEX_URL: LazyLock = + LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/rocm5.2").unwrap()); +static ROCM511_INDEX_URL: LazyLock = + LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/rocm5.1.1").unwrap()); +static ROCM42_INDEX_URL: LazyLock = + LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/rocm4.2").unwrap()); +static ROCM41_INDEX_URL: LazyLock = + LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/rocm4.1").unwrap()); +static ROCM401_INDEX_URL: LazyLock = + LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/rocm4.0.1").unwrap()); diff --git a/docs/reference/cli.md b/docs/reference/cli.md index d434b954b..9ae05a8e0 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -3349,6 +3349,22 @@ by --python-version.

  • cu91: Use the PyTorch index for CUDA 9.1
  • cu90: Use the PyTorch index for CUDA 9.0
  • cu80: Use the PyTorch index for CUDA 8.0
  • +
  • rocm6.3: Use the PyTorch index for ROCm 6.3
  • +
  • rocm6.2.4: Use the PyTorch index for ROCm 6.2.4
  • +
  • rocm6.2: Use the PyTorch index for ROCm 6.2
  • +
  • rocm6.1: Use the PyTorch index for ROCm 6.1
  • +
  • rocm6.0: Use the PyTorch index for ROCm 6.0
  • +
  • rocm5.7: Use the PyTorch index for ROCm 5.7
  • +
  • rocm5.6: Use the PyTorch index for ROCm 5.6
  • +
  • rocm5.5: Use the PyTorch index for ROCm 5.5
  • +
  • rocm5.4.2: Use the PyTorch index for ROCm 5.4.2
  • +
  • rocm5.4: Use the PyTorch index for ROCm 5.4
  • +
  • rocm5.3: Use the PyTorch index for ROCm 5.3
  • +
  • rocm5.2: Use the PyTorch index for ROCm 5.2
  • +
  • rocm5.1.1: Use the PyTorch index for ROCm 5.1.1
  • +
  • rocm4.2: Use the PyTorch index for ROCm 4.2
  • +
  • rocm4.1: Use the PyTorch index for ROCm 4.1
  • +
  • rocm4.0.1: Use the PyTorch index for ROCm 4.0.1
  • --universal

    Perform a universal resolution, attempting to generate a single requirements.txt output file that is compatible with all operating systems, architectures, and Python implementations.

    In universal mode, the current Python version (or user-provided --python-version) will be treated as a lower bound. For example, --universal --python-version 3.7 would produce a universal resolution for Python 3.7 and later.

    Implies --no-strip-markers.

    @@ -3590,6 +3606,22 @@ be used with caution, as it can modify the system Python installation.

  • cu91: Use the PyTorch index for CUDA 9.1
  • cu90: Use the PyTorch index for CUDA 9.0
  • cu80: Use the PyTorch index for CUDA 8.0
  • +
  • rocm6.3: Use the PyTorch index for ROCm 6.3
  • +
  • rocm6.2.4: Use the PyTorch index for ROCm 6.2.4
  • +
  • rocm6.2: Use the PyTorch index for ROCm 6.2
  • +
  • rocm6.1: Use the PyTorch index for ROCm 6.1
  • +
  • rocm6.0: Use the PyTorch index for ROCm 6.0
  • +
  • rocm5.7: Use the PyTorch index for ROCm 5.7
  • +
  • rocm5.6: Use the PyTorch index for ROCm 5.6
  • +
  • rocm5.5: Use the PyTorch index for ROCm 5.5
  • +
  • rocm5.4.2: Use the PyTorch index for ROCm 5.4.2
  • +
  • rocm5.4: Use the PyTorch index for ROCm 5.4
  • +
  • rocm5.3: Use the PyTorch index for ROCm 5.3
  • +
  • rocm5.2: Use the PyTorch index for ROCm 5.2
  • +
  • rocm5.1.1: Use the PyTorch index for ROCm 5.1.1
  • +
  • rocm4.2: Use the PyTorch index for ROCm 4.2
  • +
  • rocm4.1: Use the PyTorch index for ROCm 4.1
  • +
  • rocm4.0.1: Use the PyTorch index for ROCm 4.0.1
  • --verbose, -v

    Use verbose output.

    You can configure fine-grained logging using the RUST_LOG environment variable. (https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives)

    @@ -3864,6 +3896,22 @@ should be used with caution, as it can modify the system Python installation.

    cu91: Use the PyTorch index for CUDA 9.1
  • cu90: Use the PyTorch index for CUDA 9.0
  • cu80: Use the PyTorch index for CUDA 8.0
  • +
  • rocm6.3: Use the PyTorch index for ROCm 6.3
  • +
  • rocm6.2.4: Use the PyTorch index for ROCm 6.2.4
  • +
  • rocm6.2: Use the PyTorch index for ROCm 6.2
  • +
  • rocm6.1: Use the PyTorch index for ROCm 6.1
  • +
  • rocm6.0: Use the PyTorch index for ROCm 6.0
  • +
  • rocm5.7: Use the PyTorch index for ROCm 5.7
  • +
  • rocm5.6: Use the PyTorch index for ROCm 5.6
  • +
  • rocm5.5: Use the PyTorch index for ROCm 5.5
  • +
  • rocm5.4.2: Use the PyTorch index for ROCm 5.4.2
  • +
  • rocm5.4: Use the PyTorch index for ROCm 5.4
  • +
  • rocm5.3: Use the PyTorch index for ROCm 5.3
  • +
  • rocm5.2: Use the PyTorch index for ROCm 5.2
  • +
  • rocm5.1.1: Use the PyTorch index for ROCm 5.1.1
  • +
  • rocm4.2: Use the PyTorch index for ROCm 4.2
  • +
  • rocm4.1: Use the PyTorch index for ROCm 4.1
  • +
  • rocm4.0.1: Use the PyTorch index for ROCm 4.0.1
  • --upgrade, -U

    Allow package upgrades, ignoring pinned versions in any existing output file. Implies --refresh

    --upgrade-package, -P upgrade-package

    Allow upgrades for a specific package, ignoring pinned versions in any existing output file. Implies --refresh-package

    --user
    --verbose, -v

    Use verbose output.

    diff --git a/uv.schema.json b/uv.schema.json index 33c1ff1f5..0d2b47490 100644 --- a/uv.schema.json +++ b/uv.schema.json @@ -2589,6 +2589,118 @@ "enum": [ "cu80" ] + }, + { + "description": "Use the PyTorch index for ROCm 6.3.", + "type": "string", + "enum": [ + "rocm6.3" + ] + }, + { + "description": "Use the PyTorch index for ROCm 6.2.4.", + "type": "string", + "enum": [ + "rocm6.2.4" + ] + }, + { + "description": "Use the PyTorch index for ROCm 6.2.", + "type": "string", + "enum": [ + "rocm6.2" + ] + }, + { + "description": "Use the PyTorch index for ROCm 6.1.", + "type": "string", + "enum": [ + "rocm6.1" + ] + }, + { + "description": "Use the PyTorch index for ROCm 6.0.", + "type": "string", + "enum": [ + "rocm6.0" + ] + }, + { + "description": "Use the PyTorch index for ROCm 5.7.", + "type": "string", + "enum": [ + "rocm5.7" + ] + }, + { + "description": "Use the PyTorch index for ROCm 5.6.", + "type": "string", + "enum": [ + "rocm5.6" + ] + }, + { + "description": "Use the PyTorch index for ROCm 5.5.", + "type": "string", + "enum": [ + "rocm5.5" + ] + }, + { + "description": "Use the PyTorch index for ROCm 5.4.2.", + "type": "string", + "enum": [ + "rocm5.4.2" + ] + }, + { + "description": "Use the PyTorch index for ROCm 5.4.", + "type": "string", + "enum": [ + "rocm5.4" + ] + }, + { + "description": "Use the PyTorch index for ROCm 5.3.", + "type": "string", + "enum": [ + "rocm5.3" + ] + }, + { + "description": "Use the PyTorch index for ROCm 5.2.", + "type": "string", + "enum": [ + "rocm5.2" + ] + }, + { + "description": "Use the PyTorch index for ROCm 5.1.1.", + "type": "string", + "enum": [ + "rocm5.1.1" + ] + }, + { + "description": "Use the PyTorch index for ROCm 4.2.", + "type": "string", + "enum": [ + "rocm4.2" + ] + }, + { + "description": "Use the PyTorch index for ROCm 4.1.", + "type": "string", + "enum": [ + "rocm4.1" + ] + }, + { + "description": "Use the PyTorch index for ROCm 4.0.1.", + "type": "string", + "enum": [ + "rocm4.0.1" + ] } ] }, From ee0ba65eb22c6bba59ea216ff07aeb170f8d3f2a Mon Sep 17 00:00:00 2001 From: konsti Date: Wed, 18 Jun 2025 15:06:09 +0200 Subject: [PATCH 044/185] Unify test venv `python` command creation (#14117) Refactoring in preparation for https://github.com/astral-sh/uv/pull/14080 --- crates/uv/tests/it/build.rs | 3 +- crates/uv/tests/it/build_backend.rs | 49 +- crates/uv/tests/it/common/mod.rs | 46 +- crates/uv/tests/it/pip_install.rs | 5 +- crates/uv/tests/it/pip_install_scenarios.rs | 764 ++++--------------- crates/uv/tests/it/pip_sync.rs | 42 +- crates/uv/tests/it/pip_uninstall.rs | 59 +- scripts/scenarios/templates/compile.mustache | 4 +- scripts/scenarios/templates/install.mustache | 49 +- scripts/scenarios/templates/lock.mustache | 2 +- 10 files changed, 212 insertions(+), 811 deletions(-) diff --git a/crates/uv/tests/it/build.rs b/crates/uv/tests/it/build.rs index 4fe7ca9cb..706c1a681 100644 --- a/crates/uv/tests/it/build.rs +++ b/crates/uv/tests/it/build.rs @@ -7,7 +7,6 @@ use indoc::indoc; use insta::assert_snapshot; use predicates::prelude::predicate; use std::env::current_dir; -use std::process::Command; use zip::ZipArchive; #[test] @@ -1857,7 +1856,7 @@ fn build_unconfigured_setuptools() -> Result<()> { + greet==0.1.0 (from file://[TEMP_DIR]/) "###); - uv_snapshot!(context.filters(), Command::new(context.interpreter()).arg("-c").arg("import greet"), @r###" + uv_snapshot!(context.filters(), context.python_command().arg("-c").arg("import greet"), @r###" success: true exit_code: 0 ----- stdout ----- diff --git a/crates/uv/tests/it/build_backend.rs b/crates/uv/tests/it/build_backend.rs index a806dc989..c2d99ba3e 100644 --- a/crates/uv/tests/it/build_backend.rs +++ b/crates/uv/tests/it/build_backend.rs @@ -50,13 +50,9 @@ fn built_by_uv_direct_wheel() -> Result<()> { .assert() .success(); - uv_snapshot!(context - .run() - .arg("python") + uv_snapshot!(context.python_command() .arg("-c") - .arg(BUILT_BY_UV_TEST_SCRIPT) - // Python on windows - .env(EnvVars::PYTHONUTF8, "1"), @r###" + .arg(BUILT_BY_UV_TEST_SCRIPT), @r###" success: true exit_code: 0 ----- stdout ----- @@ -138,13 +134,9 @@ fn built_by_uv_direct() -> Result<()> { drop(wheel_dir); - uv_snapshot!(context - .run() - .arg("python") + uv_snapshot!(context.python_command() .arg("-c") - .arg(BUILT_BY_UV_TEST_SCRIPT) - // Python on windows - .env(EnvVars::PYTHONUTF8, "1"), @r###" + .arg(BUILT_BY_UV_TEST_SCRIPT), @r###" success: true exit_code: 0 ----- stdout ----- @@ -169,7 +161,8 @@ fn built_by_uv_editable() -> Result<()> { // Without the editable, pytest fails. context.pip_install().arg("pytest").assert().success(); - Command::new(context.interpreter()) + context + .python_command() .arg("-m") .arg("pytest") .current_dir(built_by_uv) @@ -200,7 +193,7 @@ fn built_by_uv_editable() -> Result<()> { drop(wheel_dir); // Now, pytest passes. - uv_snapshot!(Command::new(context.interpreter()) + uv_snapshot!(context.python_command() .arg("-m") .arg("pytest") // Avoid showing absolute paths and column dependent layout @@ -340,11 +333,9 @@ fn rename_module() -> Result<()> { .success(); // Importing the module with the `module-name` name succeeds. - uv_snapshot!(Command::new(context.interpreter()) + uv_snapshot!(context.python_command() .arg("-c") - .arg("import bar") - // Python on windows - .env(EnvVars::PYTHONUTF8, "1"), @r###" + .arg("import bar"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -354,11 +345,9 @@ fn rename_module() -> Result<()> { "###); // Importing the package name fails, it was overridden by `module-name`. - uv_snapshot!(Command::new(context.interpreter()) + uv_snapshot!(context.python_command() .arg("-c") - .arg("import foo") - // Python on windows - .env(EnvVars::PYTHONUTF8, "1"), @r###" + .arg("import foo"), @r###" success: false exit_code: 1 ----- stdout ----- @@ -419,11 +408,9 @@ fn rename_module_editable_build() -> Result<()> { .success(); // Importing the module with the `module-name` name succeeds. - uv_snapshot!(Command::new(context.interpreter()) + uv_snapshot!(context.python_command() .arg("-c") - .arg("import bar") - // Python on windows - .env(EnvVars::PYTHONUTF8, "1"), @r###" + .arg("import bar"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -514,11 +501,9 @@ fn build_module_name_normalization() -> Result<()> { .assert() .success(); - uv_snapshot!(Command::new(context.interpreter()) + uv_snapshot!(context.python_command() .arg("-c") - .arg("import Django_plugin") - // Python on windows - .env(EnvVars::PYTHONUTF8, "1"), @r" + .arg("import Django_plugin"), @r" success: true exit_code: 0 ----- stdout ----- @@ -728,7 +713,7 @@ fn complex_namespace_packages() -> Result<()> { " ); - uv_snapshot!(Command::new(context.interpreter()) + uv_snapshot!(context.python_command() .arg("-c") .arg("from complex_project.part_b import two; print(two())"), @r" @@ -769,7 +754,7 @@ fn complex_namespace_packages() -> Result<()> { " ); - uv_snapshot!(Command::new(context.interpreter()) + uv_snapshot!(context.python_command() .arg("-c") .arg("from complex_project.part_b import two; print(two())"), @r" diff --git a/crates/uv/tests/it/common/mod.rs b/crates/uv/tests/it/common/mod.rs index 66eb21729..f997561a9 100644 --- a/crates/uv/tests/it/common/mod.rs +++ b/crates/uv/tests/it/common/mod.rs @@ -1085,15 +1085,30 @@ impl TestContext { } pub fn interpreter(&self) -> PathBuf { - venv_to_interpreter(&self.venv) + let venv = &self.venv; + if cfg!(unix) { + venv.join("bin").join("python") + } else if cfg!(windows) { + venv.join("Scripts").join("python.exe") + } else { + unimplemented!("Only Windows and Unix are supported") + } + } + + pub fn python_command(&self) -> Command { + let mut command = self.new_command_with(&self.interpreter()); + command + // Our tests change files in <1s, so we must disable CPython bytecode caching or we'll get stale files + // https://github.com/python/cpython/issues/75953 + .arg("-B") + // Python on windows + .env(EnvVars::PYTHONUTF8, "1"); + command } /// Run the given python code and check whether it succeeds. pub fn assert_command(&self, command: &str) -> Assert { - self.new_command_with(&venv_to_interpreter(&self.venv)) - // Our tests change files in <1s, so we must disable CPython bytecode caching or we'll get stale files - // https://github.com/python/cpython/issues/75953 - .arg("-B") + self.python_command() .arg("-c") .arg(command) .current_dir(&self.temp_dir) @@ -1102,10 +1117,7 @@ impl TestContext { /// Run the given python file and check whether it succeeds. pub fn assert_file(&self, file: impl AsRef) -> Assert { - self.new_command_with(&venv_to_interpreter(&self.venv)) - // Our tests change files in <1s, so we must disable CPython bytecode caching or we'll get stale files - // https://github.com/python/cpython/issues/75953 - .arg("-B") + self.python_command() .arg(file.as_ref()) .current_dir(&self.temp_dir) .assert() @@ -1120,6 +1132,12 @@ impl TestContext { .stdout(version); } + /// Assert a package is not installed. + pub fn assert_not_installed(&self, package: &'static str) { + self.assert_command(format!("import {package}").as_str()) + .failure(); + } + /// Generate various escaped regex patterns for the given path. pub fn path_patterns(path: impl AsRef) -> Vec { let mut patterns = Vec::new(); @@ -1347,16 +1365,6 @@ pub fn venv_bin_path(venv: impl AsRef) -> PathBuf { } } -pub fn venv_to_interpreter(venv: &Path) -> PathBuf { - if cfg!(unix) { - venv.join("bin").join("python") - } else if cfg!(windows) { - venv.join("Scripts").join("python.exe") - } else { - unimplemented!("Only Windows and Unix are supported") - } -} - /// Get the path to the python interpreter for a specific python version. pub fn get_python(version: &PythonVersion) -> PathBuf { ManagedPythonInstallations::from_settings(None) diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index 569edf00e..e0876b23c 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -19,7 +19,7 @@ use wiremock::{ use crate::common::{self, decode_token}; use crate::common::{ DEFAULT_PYTHON_VERSION, TestContext, build_vendor_links_url, download_to_disk, get_bin, - uv_snapshot, venv_bin_path, venv_to_interpreter, + uv_snapshot, venv_bin_path, }; use uv_fs::Simplified; use uv_static::EnvVars; @@ -9083,8 +9083,7 @@ fn build_tag() { ); // Ensure that we choose the highest build tag (5). - uv_snapshot!(Command::new(venv_to_interpreter(&context.venv)) - .arg("-B") + uv_snapshot!(context.python_command() .arg("-c") .arg("import build_tag; build_tag.main()") .current_dir(&context.temp_dir), @r###" diff --git a/crates/uv/tests/it/pip_install_scenarios.rs b/crates/uv/tests/it/pip_install_scenarios.rs index 1a95b1caa..153d5a8fb 100644 --- a/crates/uv/tests/it/pip_install_scenarios.rs +++ b/crates/uv/tests/it/pip_install_scenarios.rs @@ -5,52 +5,20 @@ //! #![cfg(all(feature = "python", feature = "pypi", unix))] -use std::path::Path; use std::process::Command; -use assert_cmd::assert::Assert; -use assert_cmd::prelude::*; - use uv_static::EnvVars; -use crate::common::{ - TestContext, build_vendor_links_url, get_bin, packse_index_url, uv_snapshot, - venv_to_interpreter, -}; - -fn assert_command(venv: &Path, command: &str, temp_dir: &Path) -> Assert { - Command::new(venv_to_interpreter(venv)) - .arg("-c") - .arg(command) - .current_dir(temp_dir) - .assert() -} - -fn assert_installed(venv: &Path, package: &'static str, version: &'static str, temp_dir: &Path) { - assert_command( - venv, - format!("import {package} as package; print(package.__version__, end='')").as_str(), - temp_dir, - ) - .success() - .stdout(version); -} - -fn assert_not_installed(venv: &Path, package: &'static str, temp_dir: &Path) { - assert_command(venv, format!("import {package}").as_str(), temp_dir).failure(); -} +use crate::common::{TestContext, build_vendor_links_url, packse_index_url, uv_snapshot}; /// Create a `pip install` command with options shared across all scenarios. fn command(context: &TestContext) -> Command { - let mut command = Command::new(get_bin()); + let mut command = context.pip_install(); command - .arg("pip") - .arg("install") .arg("--index-url") .arg(packse_index_url()) .arg("--find-links") .arg(build_vendor_links_url()); - context.add_shared_options(&mut command, true); command.env_remove(EnvVars::UV_EXCLUDE_NEWER); command } @@ -88,11 +56,7 @@ fn requires_exact_version_does_not_exist() { ╰─▶ Because there is no version of package-a==2.0.0 and you require package-a==2.0.0, we can conclude that your requirements are unsatisfiable. "); - assert_not_installed( - &context.venv, - "requires_exact_version_does_not_exist_a", - &context.temp_dir, - ); + context.assert_not_installed("requires_exact_version_does_not_exist_a"); } /// The user requires a version of `a` greater than `1.0.0` but only smaller or equal versions exist @@ -130,11 +94,7 @@ fn requires_greater_version_does_not_exist() { ╰─▶ Because only package-a<=1.0.0 is available and you require package-a>1.0.0, we can conclude that your requirements are unsatisfiable. "); - assert_not_installed( - &context.venv, - "requires_greater_version_does_not_exist_a", - &context.temp_dir, - ); + context.assert_not_installed("requires_greater_version_does_not_exist_a"); } /// The user requires a version of `a` less than `1.0.0` but only larger versions exist @@ -174,11 +134,7 @@ fn requires_less_version_does_not_exist() { ╰─▶ Because only package-a>=2.0.0 is available and you require package-a<2.0.0, we can conclude that your requirements are unsatisfiable. "); - assert_not_installed( - &context.venv, - "requires_less_version_does_not_exist_a", - &context.temp_dir, - ); + context.assert_not_installed("requires_less_version_does_not_exist_a"); } /// The user requires any version of package `a` which does not exist. @@ -211,11 +167,7 @@ fn requires_package_does_not_exist() { ╰─▶ Because package-a was not found in the package registry and you require package-a, we can conclude that your requirements are unsatisfiable. "); - assert_not_installed( - &context.venv, - "requires_package_does_not_exist_a", - &context.temp_dir, - ); + context.assert_not_installed("requires_package_does_not_exist_a"); } /// The user requires package `a` but `a` requires package `b` which does not exist @@ -254,11 +206,7 @@ fn transitive_requires_package_does_not_exist() { And because only package-a==1.0.0 is available and you require package-a, we can conclude that your requirements are unsatisfiable. "); - assert_not_installed( - &context.venv, - "transitive_requires_package_does_not_exist_a", - &context.temp_dir, - ); + context.assert_not_installed("transitive_requires_package_does_not_exist_a"); } /// There is a non-contiguous range of compatible versions for the requested package `a`, but another dependency `c` excludes the range. This is the same as `dependency-excludes-range-of-compatible-versions` but some of the versions of `a` are incompatible for another reason e.g. dependency on non-existent package `d`. @@ -376,21 +324,12 @@ fn dependency_excludes_non_contiguous_range_of_compatible_versions() { "); // Only the `2.x` versions of `a` are available since `a==1.0.0` and `a==3.0.0` require incompatible versions of `b`, but all available versions of `c` exclude that range of `a` so resolution fails. - assert_not_installed( - &context.venv, - "dependency_excludes_non_contiguous_range_of_compatible_versions_a", - &context.temp_dir, - ); - assert_not_installed( - &context.venv, - "dependency_excludes_non_contiguous_range_of_compatible_versions_b", - &context.temp_dir, - ); - assert_not_installed( - &context.venv, - "dependency_excludes_non_contiguous_range_of_compatible_versions_c", - &context.temp_dir, - ); + context + .assert_not_installed("dependency_excludes_non_contiguous_range_of_compatible_versions_a"); + context + .assert_not_installed("dependency_excludes_non_contiguous_range_of_compatible_versions_b"); + context + .assert_not_installed("dependency_excludes_non_contiguous_range_of_compatible_versions_c"); } /// There is a range of compatible versions for the requested package `a`, but another dependency `c` excludes that range. @@ -499,21 +438,9 @@ fn dependency_excludes_range_of_compatible_versions() { "); // Only the `2.x` versions of `a` are available since `a==1.0.0` and `a==3.0.0` require incompatible versions of `b`, but all available versions of `c` exclude that range of `a` so resolution fails. - assert_not_installed( - &context.venv, - "dependency_excludes_range_of_compatible_versions_a", - &context.temp_dir, - ); - assert_not_installed( - &context.venv, - "dependency_excludes_range_of_compatible_versions_b", - &context.temp_dir, - ); - assert_not_installed( - &context.venv, - "dependency_excludes_range_of_compatible_versions_c", - &context.temp_dir, - ); + context.assert_not_installed("dependency_excludes_range_of_compatible_versions_a"); + context.assert_not_installed("dependency_excludes_range_of_compatible_versions_b"); + context.assert_not_installed("dependency_excludes_range_of_compatible_versions_c"); } /// Only one version of the requested package `a` is compatible, but the user has banned that version. @@ -586,16 +513,8 @@ fn excluded_only_compatible_version() { "); // Only `a==1.2.0` is available since `a==1.0.0` and `a==3.0.0` require incompatible versions of `b`. The user has excluded that version of `a` so resolution fails. - assert_not_installed( - &context.venv, - "excluded_only_compatible_version_a", - &context.temp_dir, - ); - assert_not_installed( - &context.venv, - "excluded_only_compatible_version_b", - &context.temp_dir, - ); + context.assert_not_installed("excluded_only_compatible_version_a"); + context.assert_not_installed("excluded_only_compatible_version_b"); } /// Only one version of the requested package is available, but the user has banned that version. @@ -635,7 +554,7 @@ fn excluded_only_version() { "); // Only `a==1.0.0` is available but the user excluded it. - assert_not_installed(&context.venv, "excluded_only_version_a", &context.temp_dir); + context.assert_not_installed("excluded_only_version_a"); } /// Multiple optional dependencies are requested for the package via an 'all' extra. @@ -701,24 +620,9 @@ fn all_extras_required() { + package-c==1.0.0 "); - assert_installed( - &context.venv, - "all_extras_required_a", - "1.0.0", - &context.temp_dir, - ); - assert_installed( - &context.venv, - "all_extras_required_b", - "1.0.0", - &context.temp_dir, - ); - assert_installed( - &context.venv, - "all_extras_required_c", - "1.0.0", - &context.temp_dir, - ); + context.assert_installed("all_extras_required_a", "1.0.0"); + context.assert_installed("all_extras_required_b", "1.0.0"); + context.assert_installed("all_extras_required_c", "1.0.0"); } /// Optional dependencies are requested for the package, the extra is only available on an older version. @@ -771,12 +675,7 @@ fn extra_does_not_exist_backtrack() { "); // The resolver should not backtrack to `a==1.0.0` because missing extras are allowed during resolution. `b` should not be installed. - assert_installed( - &context.venv, - "extra_does_not_exist_backtrack_a", - "3.0.0", - &context.temp_dir, - ); + context.assert_installed("extra_does_not_exist_backtrack_a", "3.0.0"); } /// One of two incompatible optional dependencies are requested for the package. @@ -829,18 +728,8 @@ fn extra_incompatible_with_extra_not_requested() { "); // Because the user does not request both extras, it is okay that one is incompatible with the other. - assert_installed( - &context.venv, - "extra_incompatible_with_extra_not_requested_a", - "1.0.0", - &context.temp_dir, - ); - assert_installed( - &context.venv, - "extra_incompatible_with_extra_not_requested_b", - "2.0.0", - &context.temp_dir, - ); + context.assert_installed("extra_incompatible_with_extra_not_requested_a", "1.0.0"); + context.assert_installed("extra_incompatible_with_extra_not_requested_b", "2.0.0"); } /// Multiple optional dependencies are requested for the package, but they have conflicting requirements with each other. @@ -892,11 +781,7 @@ fn extra_incompatible_with_extra() { "); // Because both `extra_b` and `extra_c` are requested and they require incompatible versions of `b`, `a` cannot be installed. - assert_not_installed( - &context.venv, - "extra_incompatible_with_extra_a", - &context.temp_dir, - ); + context.assert_not_installed("extra_incompatible_with_extra_a"); } /// Optional dependencies are requested for the package, but the extra is not compatible with other requested versions. @@ -946,16 +831,8 @@ fn extra_incompatible_with_root() { "); // Because the user requested `b==2.0.0` but the requested extra requires `b==1.0.0`, the dependencies cannot be satisfied. - assert_not_installed( - &context.venv, - "extra_incompatible_with_root_a", - &context.temp_dir, - ); - assert_not_installed( - &context.venv, - "extra_incompatible_with_root_b", - &context.temp_dir, - ); + context.assert_not_installed("extra_incompatible_with_root_a"); + context.assert_not_installed("extra_incompatible_with_root_b"); } /// Optional dependencies are requested for the package. @@ -1001,18 +878,8 @@ fn extra_required() { + package-b==1.0.0 "); - assert_installed( - &context.venv, - "extra_required_a", - "1.0.0", - &context.temp_dir, - ); - assert_installed( - &context.venv, - "extra_required_b", - "1.0.0", - &context.temp_dir, - ); + context.assert_installed("extra_required_a", "1.0.0"); + context.assert_installed("extra_required_b", "1.0.0"); } /// Optional dependencies are requested for the package, but the extra does not exist. @@ -1052,7 +919,7 @@ fn missing_extra() { "); // Missing extras are ignored during resolution. - assert_installed(&context.venv, "missing_extra_a", "1.0.0", &context.temp_dir); + context.assert_installed("missing_extra_a", "1.0.0"); } /// Multiple optional dependencies are requested for the package. @@ -1106,24 +973,9 @@ fn multiple_extras_required() { + package-c==1.0.0 "); - assert_installed( - &context.venv, - "multiple_extras_required_a", - "1.0.0", - &context.temp_dir, - ); - assert_installed( - &context.venv, - "multiple_extras_required_b", - "1.0.0", - &context.temp_dir, - ); - assert_installed( - &context.venv, - "multiple_extras_required_c", - "1.0.0", - &context.temp_dir, - ); + context.assert_installed("multiple_extras_required_a", "1.0.0"); + context.assert_installed("multiple_extras_required_b", "1.0.0"); + context.assert_installed("multiple_extras_required_c", "1.0.0"); } /// The user requires two incompatible, existing versions of package `a` @@ -1164,16 +1016,8 @@ fn direct_incompatible_versions() { ╰─▶ Because you require package-a==1.0.0 and package-a==2.0.0, we can conclude that your requirements are unsatisfiable. "); - assert_not_installed( - &context.venv, - "direct_incompatible_versions_a", - &context.temp_dir, - ); - assert_not_installed( - &context.venv, - "direct_incompatible_versions_a", - &context.temp_dir, - ); + context.assert_not_installed("direct_incompatible_versions_a"); + context.assert_not_installed("direct_incompatible_versions_a"); } /// The user requires `a`, which requires two incompatible, existing versions of package `b` @@ -1214,11 +1058,7 @@ fn transitive_incompatible_versions() { And because you require package-a==1.0.0, we can conclude that your requirements are unsatisfiable. "); - assert_not_installed( - &context.venv, - "transitive_incompatible_versions_a", - &context.temp_dir, - ); + context.assert_not_installed("transitive_incompatible_versions_a"); } /// The user requires packages `a` and `b` but `a` requires a different version of `b` @@ -1265,16 +1105,8 @@ fn transitive_incompatible_with_root_version() { And because you require package-a and package-b==1.0.0, we can conclude that your requirements are unsatisfiable. "); - assert_not_installed( - &context.venv, - "transitive_incompatible_with_root_version_a", - &context.temp_dir, - ); - assert_not_installed( - &context.venv, - "transitive_incompatible_with_root_version_b", - &context.temp_dir, - ); + context.assert_not_installed("transitive_incompatible_with_root_version_a"); + context.assert_not_installed("transitive_incompatible_with_root_version_b"); } /// The user requires package `a` and `b`; `a` and `b` require different versions of `c` @@ -1327,16 +1159,8 @@ fn transitive_incompatible_with_transitive() { And because you require package-a and package-b, we can conclude that your requirements are unsatisfiable. "); - assert_not_installed( - &context.venv, - "transitive_incompatible_with_transitive_a", - &context.temp_dir, - ); - assert_not_installed( - &context.venv, - "transitive_incompatible_with_transitive_b", - &context.temp_dir, - ); + context.assert_not_installed("transitive_incompatible_with_transitive_a"); + context.assert_not_installed("transitive_incompatible_with_transitive_b"); } /// A local version should be included in inclusive ordered comparisons. @@ -1378,12 +1202,7 @@ fn local_greater_than_or_equal() { "); // The version '1.2.3+foo' satisfies the constraint '>=1.2.3'. - assert_installed( - &context.venv, - "local_greater_than_or_equal_a", - "1.2.3+foo", - &context.temp_dir, - ); + context.assert_installed("local_greater_than_or_equal_a", "1.2.3+foo"); } /// A local version should be excluded in exclusive ordered comparisons. @@ -1419,7 +1238,7 @@ fn local_greater_than() { ╰─▶ Because only package-a==1.2.3+foo is available and you require package-a>1.2.3, we can conclude that your requirements are unsatisfiable. "); - assert_not_installed(&context.venv, "local_greater_than_a", &context.temp_dir); + context.assert_not_installed("local_greater_than_a"); } /// A local version should be included in inclusive ordered comparisons. @@ -1461,12 +1280,7 @@ fn local_less_than_or_equal() { "); // The version '1.2.3+foo' satisfies the constraint '<=1.2.3'. - assert_installed( - &context.venv, - "local_less_than_or_equal_a", - "1.2.3+foo", - &context.temp_dir, - ); + context.assert_installed("local_less_than_or_equal_a", "1.2.3+foo"); } /// A local version should be excluded in exclusive ordered comparisons. @@ -1502,7 +1316,7 @@ fn local_less_than() { ╰─▶ Because only package-a==1.2.3+foo is available and you require package-a<1.2.3, we can conclude that your requirements are unsatisfiable. "); - assert_not_installed(&context.venv, "local_less_than_a", &context.temp_dir); + context.assert_not_installed("local_less_than_a"); } /// Tests that we can select an older version with a local segment when newer versions are incompatible. @@ -1546,12 +1360,7 @@ fn local_not_latest() { + package-a==1.2.1+foo "); - assert_installed( - &context.venv, - "local_not_latest_a", - "1.2.1+foo", - &context.temp_dir, - ); + context.assert_installed("local_not_latest_a", "1.2.1+foo"); } /// If there is a 1.2.3 version with an sdist published and no compatible wheels, then the sdist will be used. @@ -1593,12 +1402,7 @@ fn local_not_used_with_sdist() { "); // The version '1.2.3' with an sdist satisfies the constraint '==1.2.3'. - assert_installed( - &context.venv, - "local_not_used_with_sdist_a", - "1.2.3+foo", - &context.temp_dir, - ); + context.assert_installed("local_not_used_with_sdist_a", "1.2.3+foo"); } /// A simple version constraint should not exclude published versions with local segments. @@ -1640,12 +1444,7 @@ fn local_simple() { "); // The version '1.2.3+foo' satisfies the constraint '==1.2.3'. - assert_installed( - &context.venv, - "local_simple_a", - "1.2.3+foo", - &context.temp_dir, - ); + context.assert_installed("local_simple_a", "1.2.3+foo"); } /// A dependency depends on a conflicting local version of a direct dependency, but we can backtrack to a compatible version. @@ -1701,18 +1500,8 @@ fn local_transitive_backtrack() { "); // Backtracking to '1.0.0' gives us compatible local versions of b. - assert_installed( - &context.venv, - "local_transitive_backtrack_a", - "1.0.0", - &context.temp_dir, - ); - assert_installed( - &context.venv, - "local_transitive_backtrack_b", - "2.0.0+foo", - &context.temp_dir, - ); + context.assert_installed("local_transitive_backtrack_a", "1.0.0"); + context.assert_installed("local_transitive_backtrack_b", "2.0.0+foo"); } /// A dependency depends on a conflicting local version of a direct dependency. @@ -1759,16 +1548,8 @@ fn local_transitive_conflicting() { And because you require package-a and package-b==2.0.0+foo, we can conclude that your requirements are unsatisfiable. "); - assert_not_installed( - &context.venv, - "local_transitive_conflicting_a", - &context.temp_dir, - ); - assert_not_installed( - &context.venv, - "local_transitive_conflicting_b", - &context.temp_dir, - ); + context.assert_not_installed("local_transitive_conflicting_a"); + context.assert_not_installed("local_transitive_conflicting_b"); } /// A transitive dependency has both a non-local and local version published, but the non-local version is unusable. @@ -1819,18 +1600,8 @@ fn local_transitive_confounding() { "); // The version '2.0.0+foo' satisfies the constraint '==2.0.0'. - assert_installed( - &context.venv, - "local_transitive_confounding_a", - "1.0.0", - &context.temp_dir, - ); - assert_installed( - &context.venv, - "local_transitive_confounding_b", - "2.0.0+foo", - &context.temp_dir, - ); + context.assert_installed("local_transitive_confounding_a", "1.0.0"); + context.assert_installed("local_transitive_confounding_b", "2.0.0+foo"); } /// A transitive constraint on a local version should match an inclusive ordered operator. @@ -1881,18 +1652,8 @@ fn local_transitive_greater_than_or_equal() { "); // The version '2.0.0+foo' satisfies both >=2.0.0 and ==2.0.0+foo. - assert_installed( - &context.venv, - "local_transitive_greater_than_or_equal_a", - "1.0.0", - &context.temp_dir, - ); - assert_installed( - &context.venv, - "local_transitive_greater_than_or_equal_b", - "2.0.0+foo", - &context.temp_dir, - ); + context.assert_installed("local_transitive_greater_than_or_equal_a", "1.0.0"); + context.assert_installed("local_transitive_greater_than_or_equal_b", "2.0.0+foo"); } /// A transitive constraint on a local version should not match an exclusive ordered operator. @@ -1939,16 +1700,8 @@ fn local_transitive_greater_than() { And because you require package-a and package-b==2.0.0+foo, we can conclude that your requirements are unsatisfiable. "); - assert_not_installed( - &context.venv, - "local_transitive_greater_than_a", - &context.temp_dir, - ); - assert_not_installed( - &context.venv, - "local_transitive_greater_than_b", - &context.temp_dir, - ); + context.assert_not_installed("local_transitive_greater_than_a"); + context.assert_not_installed("local_transitive_greater_than_b"); } /// A transitive constraint on a local version should match an inclusive ordered operator. @@ -1999,18 +1752,8 @@ fn local_transitive_less_than_or_equal() { "); // The version '2.0.0+foo' satisfies both <=2.0.0 and ==2.0.0+foo. - assert_installed( - &context.venv, - "local_transitive_less_than_or_equal_a", - "1.0.0", - &context.temp_dir, - ); - assert_installed( - &context.venv, - "local_transitive_less_than_or_equal_b", - "2.0.0+foo", - &context.temp_dir, - ); + context.assert_installed("local_transitive_less_than_or_equal_a", "1.0.0"); + context.assert_installed("local_transitive_less_than_or_equal_b", "2.0.0+foo"); } /// A transitive constraint on a local version should not match an exclusive ordered operator. @@ -2057,16 +1800,8 @@ fn local_transitive_less_than() { And because you require package-a and package-b==2.0.0+foo, we can conclude that your requirements are unsatisfiable. "); - assert_not_installed( - &context.venv, - "local_transitive_less_than_a", - &context.temp_dir, - ); - assert_not_installed( - &context.venv, - "local_transitive_less_than_b", - &context.temp_dir, - ); + context.assert_not_installed("local_transitive_less_than_a"); + context.assert_not_installed("local_transitive_less_than_b"); } /// A simple version constraint should not exclude published versions with local segments. @@ -2117,18 +1852,8 @@ fn local_transitive() { "); // The version '2.0.0+foo' satisfies both ==2.0.0 and ==2.0.0+foo. - assert_installed( - &context.venv, - "local_transitive_a", - "1.0.0", - &context.temp_dir, - ); - assert_installed( - &context.venv, - "local_transitive_b", - "2.0.0+foo", - &context.temp_dir, - ); + context.assert_installed("local_transitive_a", "1.0.0"); + context.assert_installed("local_transitive_b", "2.0.0+foo"); } /// Even if there is a 1.2.3 version published, if it is unavailable for some reason (no sdist and no compatible wheels in this case), a 1.2.3 version with a local segment should be usable instead. @@ -2170,12 +1895,7 @@ fn local_used_without_sdist() { "); // The version '1.2.3+foo' satisfies the constraint '==1.2.3'. - assert_installed( - &context.venv, - "local_used_without_sdist_a", - "1.2.3+foo", - &context.temp_dir, - ); + context.assert_installed("local_used_without_sdist_a", "1.2.3+foo"); } /// An equal version constraint should match a post-release version if the post-release version is available. @@ -2216,12 +1936,7 @@ fn post_equal_available() { "); // The version '1.2.3.post0' satisfies the constraint '==1.2.3.post0'. - assert_installed( - &context.venv, - "post_equal_available_a", - "1.2.3.post0", - &context.temp_dir, - ); + context.assert_installed("post_equal_available_a", "1.2.3.post0"); } /// An equal version constraint should not match a post-release version if the post-release version is not available. @@ -2259,11 +1974,7 @@ fn post_equal_not_available() { ╰─▶ Because there is no version of package-a==1.2.3.post0 and you require package-a==1.2.3.post0, we can conclude that your requirements are unsatisfiable. "); - assert_not_installed( - &context.venv, - "post_equal_not_available_a", - &context.temp_dir, - ); + context.assert_not_installed("post_equal_not_available_a"); } /// A greater-than-or-equal version constraint should match a post-release version if the constraint is itself a post-release version. @@ -2305,12 +2016,7 @@ fn post_greater_than_or_equal_post() { "); // The version '1.2.3.post1' satisfies the constraint '>=1.2.3.post0'. - assert_installed( - &context.venv, - "post_greater_than_or_equal_post_a", - "1.2.3.post1", - &context.temp_dir, - ); + context.assert_installed("post_greater_than_or_equal_post_a", "1.2.3.post1"); } /// A greater-than-or-equal version constraint should match a post-release version. @@ -2349,12 +2055,7 @@ fn post_greater_than_or_equal() { "); // The version '1.2.3.post1' satisfies the constraint '>=1.2.3'. - assert_installed( - &context.venv, - "post_greater_than_or_equal_a", - "1.2.3.post1", - &context.temp_dir, - ); + context.assert_installed("post_greater_than_or_equal_a", "1.2.3.post1"); } /// A greater-than version constraint should not match a post-release version if the post-release version is not available. @@ -2394,11 +2095,7 @@ fn post_greater_than_post_not_available() { ╰─▶ Because only package-a<=1.2.3.post1 is available and you require package-a>=1.2.3.post3, we can conclude that your requirements are unsatisfiable. "); - assert_not_installed( - &context.venv, - "post_greater_than_post_not_available_a", - &context.temp_dir, - ); + context.assert_not_installed("post_greater_than_post_not_available_a"); } /// A greater-than version constraint should match a post-release version if the constraint is itself a post-release version. @@ -2439,12 +2136,7 @@ fn post_greater_than_post() { "); // The version '1.2.3.post1' satisfies the constraint '>1.2.3.post0'. - assert_installed( - &context.venv, - "post_greater_than_post_a", - "1.2.3.post1", - &context.temp_dir, - ); + context.assert_installed("post_greater_than_post_a", "1.2.3.post1"); } /// A greater-than version constraint should not match a post-release version. @@ -2480,7 +2172,7 @@ fn post_greater_than() { ╰─▶ Because only package-a==1.2.3.post1 is available and you require package-a>1.2.3, we can conclude that your requirements are unsatisfiable. "); - assert_not_installed(&context.venv, "post_greater_than_a", &context.temp_dir); + context.assert_not_installed("post_greater_than_a"); } /// A less-than-or-equal version constraint should not match a post-release version. @@ -2516,11 +2208,7 @@ fn post_less_than_or_equal() { ╰─▶ Because only package-a==1.2.3.post1 is available and you require package-a<=1.2.3, we can conclude that your requirements are unsatisfiable. "); - assert_not_installed( - &context.venv, - "post_less_than_or_equal_a", - &context.temp_dir, - ); + context.assert_not_installed("post_less_than_or_equal_a"); } /// A less-than version constraint should not match a post-release version. @@ -2556,7 +2244,7 @@ fn post_less_than() { ╰─▶ Because only package-a==1.2.3.post1 is available and you require package-a<1.2.3, we can conclude that your requirements are unsatisfiable. "); - assert_not_installed(&context.venv, "post_less_than_a", &context.temp_dir); + context.assert_not_installed("post_less_than_a"); } /// A greater-than version constraint should not match a post-release version with a local version identifier. @@ -2594,11 +2282,7 @@ fn post_local_greater_than_post() { ╰─▶ Because only package-a<=1.2.3.post1+local is available and you require package-a>=1.2.3.post2, we can conclude that your requirements are unsatisfiable. "); - assert_not_installed( - &context.venv, - "post_local_greater_than_post_a", - &context.temp_dir, - ); + context.assert_not_installed("post_local_greater_than_post_a"); } /// A greater-than version constraint should not match a post-release version with a local version identifier. @@ -2636,11 +2320,7 @@ fn post_local_greater_than() { ╰─▶ Because only package-a<=1.2.3.post1+local is available and you require package-a>1.2.3, we can conclude that your requirements are unsatisfiable. "); - assert_not_installed( - &context.venv, - "post_local_greater_than_a", - &context.temp_dir, - ); + context.assert_not_installed("post_local_greater_than_a"); } /// A simple version constraint should not match a post-release version. @@ -2676,7 +2356,7 @@ fn post_simple() { ╰─▶ Because there is no version of package-a==1.2.3 and you require package-a==1.2.3, we can conclude that your requirements are unsatisfiable. "); - assert_not_installed(&context.venv, "post_simple_a", &context.temp_dir); + context.assert_not_installed("post_simple_a"); } /// The user requires `a` which has multiple prereleases available with different labels. @@ -2721,12 +2401,7 @@ fn package_multiple_prereleases_kinds() { "); // Release candidates should be the highest precedence prerelease kind. - assert_installed( - &context.venv, - "package_multiple_prereleases_kinds_a", - "1.0.0rc1", - &context.temp_dir, - ); + context.assert_installed("package_multiple_prereleases_kinds_a", "1.0.0rc1"); } /// The user requires `a` which has multiple alphas available. @@ -2771,12 +2446,7 @@ fn package_multiple_prereleases_numbers() { "); // The latest alpha version should be selected. - assert_installed( - &context.venv, - "package_multiple_prereleases_numbers_a", - "1.0.0a3", - &context.temp_dir, - ); + context.assert_installed("package_multiple_prereleases_numbers_a", "1.0.0a3"); } /// The user requires a non-prerelease version of `a` which only has prerelease versions available. There are pre-releases on the boundary of their range. @@ -2819,12 +2489,7 @@ fn package_only_prereleases_boundary() { "); // Since there are only prerelease versions of `a` available, a prerelease is allowed. Since the user did not explicitly request a pre-release, pre-releases at the boundary should not be selected. - assert_installed( - &context.venv, - "package_only_prereleases_boundary_a", - "0.1.0a1", - &context.temp_dir, - ); + context.assert_installed("package_only_prereleases_boundary_a", "0.1.0a1"); } /// The user requires a version of package `a` which only matches prerelease versions but they did not include a prerelease specifier. @@ -2865,11 +2530,7 @@ fn package_only_prereleases_in_range() { "); // Since there are stable versions of `a` available, prerelease versions should not be selected without explicit opt-in. - assert_not_installed( - &context.venv, - "package_only_prereleases_in_range_a", - &context.temp_dir, - ); + context.assert_not_installed("package_only_prereleases_in_range_a"); } /// The user requires any version of package `a` which only has prerelease versions available. @@ -2908,12 +2569,7 @@ fn package_only_prereleases() { "); // Since there are only prerelease versions of `a` available, it should be installed even though the user did not include a prerelease specifier. - assert_installed( - &context.venv, - "package_only_prereleases_a", - "1.0.0a1", - &context.temp_dir, - ); + context.assert_installed("package_only_prereleases_a", "1.0.0a1"); } /// The user requires a version of `a` with a prerelease specifier and both prerelease and stable releases are available. @@ -2961,12 +2617,7 @@ fn package_prerelease_specified_mixed_available() { "); // Since the user provided a prerelease specifier, the latest prerelease version should be selected. - assert_installed( - &context.venv, - "package_prerelease_specified_mixed_available_a", - "1.0.0a1", - &context.temp_dir, - ); + context.assert_installed("package_prerelease_specified_mixed_available_a", "1.0.0a1"); } /// The user requires a version of `a` with a prerelease specifier and only stable releases are available. @@ -3014,11 +2665,9 @@ fn package_prerelease_specified_only_final_available() { "); // The latest stable version should be selected. - assert_installed( - &context.venv, + context.assert_installed( "package_prerelease_specified_only_final_available_a", "0.3.0", - &context.temp_dir, ); } @@ -3067,11 +2716,9 @@ fn package_prerelease_specified_only_prerelease_available() { "); // The latest prerelease version should be selected. - assert_installed( - &context.venv, + context.assert_installed( "package_prerelease_specified_only_prerelease_available_a", "0.3.0a1", - &context.temp_dir, ); } @@ -3116,12 +2763,7 @@ fn package_prereleases_boundary() { "); // Since the user did not use a pre-release specifier, pre-releases at the boundary should not be selected even though pre-releases are allowed. - assert_installed( - &context.venv, - "package_prereleases_boundary_a", - "0.1.0", - &context.temp_dir, - ); + context.assert_installed("package_prereleases_boundary_a", "0.1.0"); } /// The user requires a non-prerelease version of `a` but has enabled pre-releases. There are pre-releases on the boundary of their range. @@ -3165,12 +2807,7 @@ fn package_prereleases_global_boundary() { "); // Since the user did not use a pre-release specifier, pre-releases at the boundary should not be selected even though pre-releases are allowed. - assert_installed( - &context.venv, - "package_prereleases_global_boundary_a", - "0.1.0", - &context.temp_dir, - ); + context.assert_installed("package_prereleases_global_boundary_a", "0.1.0"); } /// The user requires a prerelease version of `a`. There are pre-releases on the boundary of their range. @@ -3220,12 +2857,7 @@ fn package_prereleases_specifier_boundary() { "); // Since the user used a pre-release specifier, pre-releases at the boundary should be selected. - assert_installed( - &context.venv, - "package_prereleases_specifier_boundary_a", - "0.2.0a1", - &context.temp_dir, - ); + context.assert_installed("package_prereleases_specifier_boundary_a", "0.2.0a1"); } /// The user requires a version of package `a` which only matches prerelease versions. They did not include a prerelease specifier for the package, but they opted into prereleases globally. @@ -3269,11 +2901,9 @@ fn requires_package_only_prereleases_in_range_global_opt_in() { + package-a==1.0.0a1 "); - assert_installed( - &context.venv, + context.assert_installed( "requires_package_only_prereleases_in_range_global_opt_in_a", "1.0.0a1", - &context.temp_dir, ); } @@ -3315,12 +2945,7 @@ fn requires_package_prerelease_and_final_any() { "); // Since the user did not provide a prerelease specifier, the older stable version should be selected. - assert_installed( - &context.venv, - "requires_package_prerelease_and_final_any_a", - "0.1.0", - &context.temp_dir, - ); + context.assert_installed("requires_package_prerelease_and_final_any_a", "0.1.0"); } /// The user requires package `a` which has a dependency on a package which only matches prerelease versions; the user has opted into allowing prereleases in `b` explicitly. @@ -3374,17 +2999,13 @@ fn transitive_package_only_prereleases_in_range_opt_in() { "); // Since the user included a dependency on `b` with a prerelease specifier, a prerelease version can be selected. - assert_installed( - &context.venv, + context.assert_installed( "transitive_package_only_prereleases_in_range_opt_in_a", "0.1.0", - &context.temp_dir, ); - assert_installed( - &context.venv, + context.assert_installed( "transitive_package_only_prereleases_in_range_opt_in_b", "1.0.0a1", - &context.temp_dir, ); } @@ -3432,11 +3053,7 @@ fn transitive_package_only_prereleases_in_range() { "); // Since there are stable versions of `b` available, the prerelease version should not be selected without explicit opt-in. The available version is excluded by the range requested by the user. - assert_not_installed( - &context.venv, - "transitive_package_only_prereleases_in_range_a", - &context.temp_dir, - ); + context.assert_not_installed("transitive_package_only_prereleases_in_range_a"); } /// The user requires any version of package `a` which requires `b` which only has prerelease versions available. @@ -3481,18 +3098,8 @@ fn transitive_package_only_prereleases() { "); // Since there are only prerelease versions of `b` available, it should be selected even though the user did not opt-in to prereleases. - assert_installed( - &context.venv, - "transitive_package_only_prereleases_a", - "0.1.0", - &context.temp_dir, - ); - assert_installed( - &context.venv, - "transitive_package_only_prereleases_b", - "1.0.0a1", - &context.temp_dir, - ); + context.assert_installed("transitive_package_only_prereleases_a", "0.1.0"); + context.assert_installed("transitive_package_only_prereleases_b", "1.0.0a1"); } /// A transitive dependency has both a prerelease and a stable selector, but can only be satisfied by a prerelease. There are many prerelease versions and some are excluded. @@ -3605,16 +3212,10 @@ fn transitive_prerelease_and_stable_dependency_many_versions_holes() { "); // Since the user did not explicitly opt-in to a prerelease, it cannot be selected. - assert_not_installed( - &context.venv, - "transitive_prerelease_and_stable_dependency_many_versions_holes_a", - &context.temp_dir, - ); - assert_not_installed( - &context.venv, - "transitive_prerelease_and_stable_dependency_many_versions_holes_b", - &context.temp_dir, - ); + context + .assert_not_installed("transitive_prerelease_and_stable_dependency_many_versions_holes_a"); + context + .assert_not_installed("transitive_prerelease_and_stable_dependency_many_versions_holes_b"); } /// A transitive dependency has both a prerelease and a stable selector, but can only be satisfied by a prerelease. There are many prerelease versions. @@ -3716,16 +3317,8 @@ fn transitive_prerelease_and_stable_dependency_many_versions() { "); // Since the user did not explicitly opt-in to a prerelease, it cannot be selected. - assert_not_installed( - &context.venv, - "transitive_prerelease_and_stable_dependency_many_versions_a", - &context.temp_dir, - ); - assert_not_installed( - &context.venv, - "transitive_prerelease_and_stable_dependency_many_versions_b", - &context.temp_dir, - ); + context.assert_not_installed("transitive_prerelease_and_stable_dependency_many_versions_a"); + context.assert_not_installed("transitive_prerelease_and_stable_dependency_many_versions_b"); } /// A transitive dependency has both a prerelease and a stable selector, but can only be satisfied by a prerelease. The user includes an opt-in to prereleases of the transitive dependency. @@ -3788,23 +3381,17 @@ fn transitive_prerelease_and_stable_dependency_opt_in() { "); // Since the user explicitly opted-in to a prerelease for `c`, it can be installed. - assert_installed( - &context.venv, + context.assert_installed( "transitive_prerelease_and_stable_dependency_opt_in_a", "1.0.0", - &context.temp_dir, ); - assert_installed( - &context.venv, + context.assert_installed( "transitive_prerelease_and_stable_dependency_opt_in_b", "1.0.0", - &context.temp_dir, ); - assert_installed( - &context.venv, + context.assert_installed( "transitive_prerelease_and_stable_dependency_opt_in_c", "2.0.0b1", - &context.temp_dir, ); } @@ -3860,16 +3447,8 @@ fn transitive_prerelease_and_stable_dependency() { "); // Since the user did not explicitly opt-in to a prerelease, it cannot be selected. - assert_not_installed( - &context.venv, - "transitive_prerelease_and_stable_dependency_a", - &context.temp_dir, - ); - assert_not_installed( - &context.venv, - "transitive_prerelease_and_stable_dependency_b", - &context.temp_dir, - ); + context.assert_not_installed("transitive_prerelease_and_stable_dependency_a"); + context.assert_not_installed("transitive_prerelease_and_stable_dependency_b"); } /// The user requires a package where recent versions require a Python version greater than the current version, but an older version is compatible. @@ -3915,12 +3494,7 @@ fn python_greater_than_current_backtrack() { + package-a==1.0.0 "); - assert_installed( - &context.venv, - "python_greater_than_current_backtrack_a", - "1.0.0", - &context.temp_dir, - ); + context.assert_installed("python_greater_than_current_backtrack_a", "1.0.0"); } /// The user requires a package where recent versions require a Python version greater than the current version, but an excluded older version is compatible. @@ -3975,11 +3549,7 @@ fn python_greater_than_current_excluded() { And because you require package-a>=2.0.0, we can conclude that your requirements are unsatisfiable. "); - assert_not_installed( - &context.venv, - "python_greater_than_current_excluded_a", - &context.temp_dir, - ); + context.assert_not_installed("python_greater_than_current_excluded_a"); } /// The user requires a package which has many versions which all require a Python version greater than the current version @@ -4037,11 +3607,7 @@ fn python_greater_than_current_many() { ╰─▶ Because there is no version of package-a==1.0.0 and you require package-a==1.0.0, we can conclude that your requirements are unsatisfiable. "); - assert_not_installed( - &context.venv, - "python_greater_than_current_many_a", - &context.temp_dir, - ); + context.assert_not_installed("python_greater_than_current_many_a"); } /// The user requires a package which requires a Python version with a patch version greater than the current patch version @@ -4079,11 +3645,7 @@ fn python_greater_than_current_patch() { And because you require package-a==1.0.0, we can conclude that your requirements are unsatisfiable. "); - assert_not_installed( - &context.venv, - "python_greater_than_current_patch_a", - &context.temp_dir, - ); + context.assert_not_installed("python_greater_than_current_patch_a"); } /// The user requires a package which requires a Python version greater than the current version @@ -4120,11 +3682,7 @@ fn python_greater_than_current() { And because you require package-a==1.0.0, we can conclude that your requirements are unsatisfiable. "); - assert_not_installed( - &context.venv, - "python_greater_than_current_a", - &context.temp_dir, - ); + context.assert_not_installed("python_greater_than_current_a"); } /// The user requires a package which requires a Python version less than the current version @@ -4199,11 +3757,7 @@ fn python_version_does_not_exist() { And because you require package-a==1.0.0, we can conclude that your requirements are unsatisfiable. "); - assert_not_installed( - &context.venv, - "python_version_does_not_exist_a", - &context.temp_dir, - ); + context.assert_not_installed("python_version_does_not_exist_a"); } /// Both wheels and source distributions are available, and the user has disabled binaries. @@ -4323,11 +3877,7 @@ fn no_sdist_no_wheels_with_matching_abi() { hint: You require CPython 3.12 (`cp312`), but we only found wheels for `package-a` (v1.0.0) with the following Python ABI tag: `graalpy240_310_native` "); - assert_not_installed( - &context.venv, - "no_sdist_no_wheels_with_matching_abi_a", - &context.temp_dir, - ); + context.assert_not_installed("no_sdist_no_wheels_with_matching_abi_a"); } /// No wheels with matching platform tags are available, nor are any source distributions available @@ -4367,11 +3917,7 @@ fn no_sdist_no_wheels_with_matching_platform() { hint: Wheels are available for `package-a` (v1.0.0) on the following platform: `macosx_10_0_ppc64` "); - assert_not_installed( - &context.venv, - "no_sdist_no_wheels_with_matching_platform_a", - &context.temp_dir, - ); + context.assert_not_installed("no_sdist_no_wheels_with_matching_platform_a"); } /// No wheels with matching Python tags are available, nor are any source distributions available @@ -4411,11 +3957,7 @@ fn no_sdist_no_wheels_with_matching_python() { hint: You require CPython 3.12 (`cp312`), but we only found wheels for `package-a` (v1.0.0) with the following Python implementation tag: `graalpy310` "); - assert_not_installed( - &context.venv, - "no_sdist_no_wheels_with_matching_python_a", - &context.temp_dir, - ); + context.assert_not_installed("no_sdist_no_wheels_with_matching_python_a"); } /// No wheels are available, only source distributions but the user has disabled builds. @@ -4456,7 +3998,7 @@ fn no_wheels_no_build() { hint: Wheels are required for `package-a` because building from source is disabled for `package-a` (i.e., with `--no-build-package package-a`) "); - assert_not_installed(&context.venv, "no_wheels_no_build_a", &context.temp_dir); + context.assert_not_installed("no_wheels_no_build_a"); } /// No wheels with matching platform tags are available, just source distributions. @@ -4569,7 +4111,7 @@ fn only_wheels_no_binary() { hint: A source distribution is required for `package-a` because using pre-built wheels is disabled for `package-a` (i.e., with `--no-binary-package package-a`) "); - assert_not_installed(&context.venv, "only_wheels_no_binary_a", &context.temp_dir); + context.assert_not_installed("only_wheels_no_binary_a"); } /// No source distributions are available, only wheels. @@ -4684,11 +4226,7 @@ fn package_only_yanked_in_range() { "); // Since there are other versions of `a` available, yanked versions should not be selected without explicit opt-in. - assert_not_installed( - &context.venv, - "package_only_yanked_in_range_a", - &context.temp_dir, - ); + context.assert_not_installed("package_only_yanked_in_range_a"); } /// The user requires any version of package `a` which only has yanked versions available. @@ -4726,7 +4264,7 @@ fn package_only_yanked() { "); // Yanked versions should not be installed, even if they are the only one available. - assert_not_installed(&context.venv, "package_only_yanked_a", &context.temp_dir); + context.assert_not_installed("package_only_yanked_a"); } /// The user requires any version of `a` and both yanked and unyanked releases are available. @@ -4772,12 +4310,7 @@ fn package_yanked_specified_mixed_available() { "); // The latest unyanked version should be selected. - assert_installed( - &context.venv, - "package_yanked_specified_mixed_available_a", - "0.3.0", - &context.temp_dir, - ); + context.assert_installed("package_yanked_specified_mixed_available_a", "0.3.0"); } /// The user requires any version of package `a` has a yanked version available and an older unyanked version. @@ -4818,12 +4351,7 @@ fn requires_package_yanked_and_unyanked_any() { "); // The unyanked version should be selected. - assert_installed( - &context.venv, - "requires_package_yanked_and_unyanked_any_a", - "0.1.0", - &context.temp_dir, - ); + context.assert_installed("requires_package_yanked_and_unyanked_any_a", "0.1.0"); } /// The user requires package `a` which has a dependency on a package which only matches yanked versions; the user has opted into allowing the yanked version of `b` explicitly. @@ -4877,18 +4405,8 @@ fn transitive_package_only_yanked_in_range_opt_in() { "#); // Since the user included a dependency on `b` with an exact specifier, the yanked version can be selected. - assert_installed( - &context.venv, - "transitive_package_only_yanked_in_range_opt_in_a", - "0.1.0", - &context.temp_dir, - ); - assert_installed( - &context.venv, - "transitive_package_only_yanked_in_range_opt_in_b", - "1.0.0", - &context.temp_dir, - ); + context.assert_installed("transitive_package_only_yanked_in_range_opt_in_a", "0.1.0"); + context.assert_installed("transitive_package_only_yanked_in_range_opt_in_b", "1.0.0"); } /// The user requires package `a` which has a dependency on a package which only matches yanked versions. @@ -4937,11 +4455,7 @@ fn transitive_package_only_yanked_in_range() { "); // Yanked versions should not be installed, even if they are the only valid version in a range. - assert_not_installed( - &context.venv, - "transitive_package_only_yanked_in_range_a", - &context.temp_dir, - ); + context.assert_not_installed("transitive_package_only_yanked_in_range_a"); } /// The user requires any version of package `a` which requires `b` which only has yanked versions available. @@ -4985,11 +4499,7 @@ fn transitive_package_only_yanked() { "); // Yanked versions should not be installed, even if they are the only one available. - assert_not_installed( - &context.venv, - "transitive_package_only_yanked_a", - &context.temp_dir, - ); + context.assert_not_installed("transitive_package_only_yanked_a"); } /// A transitive dependency has both a yanked and an unyanked version, but can only be satisfied by a yanked. The user includes an opt-in to the yanked version of the transitive dependency. @@ -5052,23 +4562,17 @@ fn transitive_yanked_and_unyanked_dependency_opt_in() { "#); // Since the user explicitly selected the yanked version of `c`, it can be installed. - assert_installed( - &context.venv, + context.assert_installed( "transitive_yanked_and_unyanked_dependency_opt_in_a", "1.0.0", - &context.temp_dir, ); - assert_installed( - &context.venv, + context.assert_installed( "transitive_yanked_and_unyanked_dependency_opt_in_b", "1.0.0", - &context.temp_dir, ); - assert_installed( - &context.venv, + context.assert_installed( "transitive_yanked_and_unyanked_dependency_opt_in_c", "2.0.0", - &context.temp_dir, ); } @@ -5122,14 +4626,6 @@ fn transitive_yanked_and_unyanked_dependency() { "); // Since the user did not explicitly select the yanked version, it cannot be used. - assert_not_installed( - &context.venv, - "transitive_yanked_and_unyanked_dependency_a", - &context.temp_dir, - ); - assert_not_installed( - &context.venv, - "transitive_yanked_and_unyanked_dependency_b", - &context.temp_dir, - ); + context.assert_not_installed("transitive_yanked_and_unyanked_dependency_a"); + context.assert_not_installed("transitive_yanked_and_unyanked_dependency_b"); } diff --git a/crates/uv/tests/it/pip_sync.rs b/crates/uv/tests/it/pip_sync.rs index 32252396e..43cbc26c7 100644 --- a/crates/uv/tests/it/pip_sync.rs +++ b/crates/uv/tests/it/pip_sync.rs @@ -1,6 +1,4 @@ use std::env::consts::EXE_SUFFIX; -use std::path::Path; -use std::process::Command; use anyhow::Result; use assert_cmd::prelude::*; @@ -11,24 +9,10 @@ use indoc::indoc; use predicates::Predicate; use url::Url; -use crate::common::{ - TestContext, download_to_disk, site_packages_path, uv_snapshot, venv_to_interpreter, -}; +use crate::common::{TestContext, download_to_disk, site_packages_path, uv_snapshot}; use uv_fs::{Simplified, copy_dir_all}; use uv_static::EnvVars; -fn check_command(venv: &Path, command: &str, temp_dir: &Path) { - Command::new(venv_to_interpreter(venv)) - // Our tests change files in <1s, so we must disable CPython bytecode caching or we'll get stale files - // https://github.com/python/cpython/issues/75953 - .arg("-B") - .arg("-c") - .arg(command) - .current_dir(temp_dir) - .assert() - .success(); -} - #[test] fn missing_requirements_txt() { let context = TestContext::new("3.12"); @@ -463,7 +447,13 @@ fn link() -> Result<()> { "### ); - check_command(&context2.venv, "import iniconfig", &context2.temp_dir); + context2 + .python_command() + .arg("-c") + .arg("import iniconfig") + .current_dir(&context2.temp_dir) + .assert() + .success(); Ok(()) } @@ -5221,8 +5211,8 @@ fn target_built_distribution() -> Result<()> { context.assert_command("import iniconfig").failure(); // Ensure that we can import the package by augmenting the `PYTHONPATH`. - Command::new(venv_to_interpreter(&context.venv)) - .arg("-B") + context + .python_command() .arg("-c") .arg("import iniconfig") .env(EnvVars::PYTHONPATH, context.temp_dir.child("target").path()) @@ -5326,8 +5316,8 @@ fn target_source_distribution() -> Result<()> { context.assert_command("import iniconfig").failure(); // Ensure that we can import the package by augmenting the `PYTHONPATH`. - Command::new(venv_to_interpreter(&context.venv)) - .arg("-B") + context + .python_command() .arg("-c") .arg("import iniconfig") .env(EnvVars::PYTHONPATH, context.temp_dir.child("target").path()) @@ -5397,8 +5387,8 @@ fn target_no_build_isolation() -> Result<()> { context.assert_command("import wheel").failure(); // Ensure that we can import the package by augmenting the `PYTHONPATH`. - Command::new(venv_to_interpreter(&context.venv)) - .arg("-B") + context + .python_command() .arg("-c") .arg("import wheel") .env(EnvVars::PYTHONPATH, context.temp_dir.child("target").path()) @@ -5474,8 +5464,8 @@ fn prefix() -> Result<()> { context.assert_command("import iniconfig").failure(); // Ensure that we can import the package by augmenting the `PYTHONPATH`. - Command::new(venv_to_interpreter(&context.venv)) - .arg("-B") + context + .python_command() .arg("-c") .arg("import iniconfig") .env( diff --git a/crates/uv/tests/it/pip_uninstall.rs b/crates/uv/tests/it/pip_uninstall.rs index 3c1c0d717..5e6cbf6f9 100644 --- a/crates/uv/tests/it/pip_uninstall.rs +++ b/crates/uv/tests/it/pip_uninstall.rs @@ -5,7 +5,7 @@ use assert_cmd::prelude::*; use assert_fs::fixture::ChildPath; use assert_fs::prelude::*; -use crate::common::{TestContext, get_bin, uv_snapshot, venv_to_interpreter}; +use crate::common::{TestContext, get_bin, uv_snapshot}; #[test] fn no_arguments() { @@ -113,12 +113,7 @@ fn uninstall() -> Result<()> { .assert() .success(); - Command::new(venv_to_interpreter(&context.venv)) - .arg("-c") - .arg("import markupsafe") - .current_dir(&context.temp_dir) - .assert() - .success(); + context.assert_command("import markupsafe").success(); uv_snapshot!(context.pip_uninstall() .arg("MarkupSafe"), @r###" @@ -132,12 +127,7 @@ fn uninstall() -> Result<()> { "### ); - Command::new(venv_to_interpreter(&context.venv)) - .arg("-c") - .arg("import markupsafe") - .current_dir(&context.temp_dir) - .assert() - .failure(); + context.assert_command("import markupsafe").failure(); Ok(()) } @@ -156,12 +146,7 @@ fn missing_record() -> Result<()> { .assert() .success(); - Command::new(venv_to_interpreter(&context.venv)) - .arg("-c") - .arg("import markupsafe") - .current_dir(&context.temp_dir) - .assert() - .success(); + context.assert_command("import markupsafe").success(); // Delete the RECORD file. let dist_info = context.site_packages().join("MarkupSafe-2.1.3.dist-info"); @@ -202,11 +187,7 @@ fn uninstall_editable_by_name() -> Result<()> { .assert() .success(); - Command::new(venv_to_interpreter(&context.venv)) - .arg("-c") - .arg("import poetry_editable") - .assert() - .success(); + context.assert_command("import poetry_editable").success(); // Uninstall the editable by name. uv_snapshot!(context.filters(), context.pip_uninstall() @@ -221,11 +202,7 @@ fn uninstall_editable_by_name() -> Result<()> { "### ); - Command::new(venv_to_interpreter(&context.venv)) - .arg("-c") - .arg("import poetry_editable") - .assert() - .failure(); + context.assert_command("import poetry_editable").failure(); Ok(()) } @@ -251,11 +228,7 @@ fn uninstall_by_path() -> Result<()> { .assert() .success(); - Command::new(venv_to_interpreter(&context.venv)) - .arg("-c") - .arg("import poetry_editable") - .assert() - .success(); + context.assert_command("import poetry_editable").success(); // Uninstall the editable by path. uv_snapshot!(context.filters(), context.pip_uninstall() @@ -270,11 +243,7 @@ fn uninstall_by_path() -> Result<()> { "### ); - Command::new(venv_to_interpreter(&context.venv)) - .arg("-c") - .arg("import poetry_editable") - .assert() - .failure(); + context.assert_command("import poetry_editable").failure(); Ok(()) } @@ -300,11 +269,7 @@ fn uninstall_duplicate_by_path() -> Result<()> { .assert() .success(); - Command::new(venv_to_interpreter(&context.venv)) - .arg("-c") - .arg("import poetry_editable") - .assert() - .success(); + context.assert_command("import poetry_editable").success(); // Uninstall the editable by both path and name. uv_snapshot!(context.filters(), context.pip_uninstall() @@ -320,11 +285,7 @@ fn uninstall_duplicate_by_path() -> Result<()> { "### ); - Command::new(venv_to_interpreter(&context.venv)) - .arg("-c") - .arg("import poetry_editable") - .assert() - .failure(); + context.assert_command("import poetry_editable").failure(); Ok(()) } diff --git a/scripts/scenarios/templates/compile.mustache b/scripts/scenarios/templates/compile.mustache index aa6db8529..2a3202662 100644 --- a/scripts/scenarios/templates/compile.mustache +++ b/scripts/scenarios/templates/compile.mustache @@ -16,8 +16,8 @@ use predicates::prelude::predicate; use uv_static::EnvVars; use crate::common::{ - build_vendor_links_url, get_bin, packse_index_url, python_path_with_versions, uv_snapshot, - TestContext, + TestContext, build_vendor_links_url, get_bin, packse_index_url, python_path_with_versions, + uv_snapshot, }; /// Provision python binaries and return a `pip compile` command with options shared across all scenarios. diff --git a/scripts/scenarios/templates/install.mustache b/scripts/scenarios/templates/install.mustache index 15f48077e..8f1c477b2 100644 --- a/scripts/scenarios/templates/install.mustache +++ b/scripts/scenarios/templates/install.mustache @@ -5,52 +5,20 @@ //! #![cfg(all(feature = "python", feature = "pypi", unix))] -use std::path::Path; use std::process::Command; -use assert_cmd::assert::Assert; -use assert_cmd::prelude::*; - use uv_static::EnvVars; -use crate::common::{ - build_vendor_links_url, get_bin, packse_index_url, uv_snapshot, venv_to_interpreter, - TestContext, -}; - -fn assert_command(venv: &Path, command: &str, temp_dir: &Path) -> Assert { - Command::new(venv_to_interpreter(venv)) - .arg("-c") - .arg(command) - .current_dir(temp_dir) - .assert() -} - -fn assert_installed(venv: &Path, package: &'static str, version: &'static str, temp_dir: &Path) { - assert_command( - venv, - format!("import {package} as package; print(package.__version__, end='')").as_str(), - temp_dir, - ) - .success() - .stdout(version); -} - -fn assert_not_installed(venv: &Path, package: &'static str, temp_dir: &Path) { - assert_command(venv, format!("import {package}").as_str(), temp_dir).failure(); -} +use crate::common::{TestContext, build_vendor_links_url, packse_index_url, uv_snapshot}; /// Create a `pip install` command with options shared across all scenarios. fn command(context: &TestContext) -> Command { - let mut command = Command::new(get_bin()); + let mut command = context.pip_install(); command - .arg("pip") - .arg("install") .arg("--index-url") .arg(packse_index_url()) .arg("--find-links") .arg(build_vendor_links_url()); - context.add_shared_options(&mut command, true); command.env_remove(EnvVars::UV_EXCLUDE_NEWER); command } @@ -93,25 +61,20 @@ fn {{module_name}}() { {{/resolver_options.python_platform}} {{#root.requires}} .arg("{{requirement}}") - {{/root.requires}}, @r###" - "###); + {{/root.requires}}, @r#" + "#); {{#expected.explanation}} // {{expected.explanation}} {{/expected.explanation}} {{#expected.satisfiable}} {{#expected.packages}} - assert_installed( - &context.venv, - "{{module_name}}", - "{{version}}", - &context.temp_dir - ); + context.assert_installed("{{module_name}}", "{{version}}"); {{/expected.packages}} {{/expected.satisfiable}} {{^expected.satisfiable}} {{#root.requires}} - assert_not_installed(&context.venv, "{{module_name}}", &context.temp_dir); + context.assert_not_installed("{{module_name}}"); {{/root.requires}} {{/expected.satisfiable}} } diff --git a/scripts/scenarios/templates/lock.mustache b/scripts/scenarios/templates/lock.mustache index 7d80b8f9b..74deb3764 100644 --- a/scripts/scenarios/templates/lock.mustache +++ b/scripts/scenarios/templates/lock.mustache @@ -15,7 +15,7 @@ use insta::assert_snapshot; use uv_static::EnvVars; -use crate::common::{packse_index_url, TestContext, uv_snapshot}; +use crate::common::{TestContext, packse_index_url, uv_snapshot}; {{#scenarios}} From 611a13c8415dc1739ee7c33a7375e846871bc4b2 Mon Sep 17 00:00:00 2001 From: John Mumm Date: Wed, 18 Jun 2025 10:30:12 -0400 Subject: [PATCH 045/185] Fix benchmark compilation failure: `cannot find attribute clap in this scope` (#14128) [Two benchmark jobs](https://github.com/astral-sh/uv/actions/runs/15732775460/job/44337710992?pr=14126) were failing with `error: cannot find attribute clap in this scope` based on #14120. This updates the recently added `#[clap(name = rocm...` lines to use `cfg_attr(feature = "clap",`. --- crates/uv-torch/src/backend.rs | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/crates/uv-torch/src/backend.rs b/crates/uv-torch/src/backend.rs index 263ea07bd..958ea3d9e 100644 --- a/crates/uv-torch/src/backend.rs +++ b/crates/uv-torch/src/backend.rs @@ -109,67 +109,67 @@ pub enum TorchMode { Cu80, /// Use the PyTorch index for ROCm 6.3. #[serde(rename = "rocm6.3")] - #[clap(name = "rocm6.3")] + #[cfg_attr(feature = "clap", clap(name = "rocm6.3"))] Rocm63, /// Use the PyTorch index for ROCm 6.2.4. #[serde(rename = "rocm6.2.4")] - #[clap(name = "rocm6.2.4")] + #[cfg_attr(feature = "clap", clap(name = "rocm6.2.4"))] Rocm624, /// Use the PyTorch index for ROCm 6.2. #[serde(rename = "rocm6.2")] - #[clap(name = "rocm6.2")] + #[cfg_attr(feature = "clap", clap(name = "rocm6.2"))] Rocm62, /// Use the PyTorch index for ROCm 6.1. #[serde(rename = "rocm6.1")] - #[clap(name = "rocm6.1")] + #[cfg_attr(feature = "clap", clap(name = "rocm6.1"))] Rocm61, /// Use the PyTorch index for ROCm 6.0. #[serde(rename = "rocm6.0")] - #[clap(name = "rocm6.0")] + #[cfg_attr(feature = "clap", clap(name = "rocm6.0"))] Rocm60, /// Use the PyTorch index for ROCm 5.7. #[serde(rename = "rocm5.7")] - #[clap(name = "rocm5.7")] + #[cfg_attr(feature = "clap", clap(name = "rocm5.7"))] Rocm57, /// Use the PyTorch index for ROCm 5.6. #[serde(rename = "rocm5.6")] - #[clap(name = "rocm5.6")] + #[cfg_attr(feature = "clap", clap(name = "rocm5.6"))] Rocm56, /// Use the PyTorch index for ROCm 5.5. #[serde(rename = "rocm5.5")] - #[clap(name = "rocm5.5")] + #[cfg_attr(feature = "clap", clap(name = "rocm5.5"))] Rocm55, /// Use the PyTorch index for ROCm 5.4.2. #[serde(rename = "rocm5.4.2")] - #[clap(name = "rocm5.4.2")] + #[cfg_attr(feature = "clap", clap(name = "rocm5.4.2"))] Rocm542, /// Use the PyTorch index for ROCm 5.4. #[serde(rename = "rocm5.4")] - #[clap(name = "rocm5.4")] + #[cfg_attr(feature = "clap", clap(name = "rocm5.4"))] Rocm54, /// Use the PyTorch index for ROCm 5.3. #[serde(rename = "rocm5.3")] - #[clap(name = "rocm5.3")] + #[cfg_attr(feature = "clap", clap(name = "rocm5.3"))] Rocm53, /// Use the PyTorch index for ROCm 5.2. #[serde(rename = "rocm5.2")] - #[clap(name = "rocm5.2")] + #[cfg_attr(feature = "clap", clap(name = "rocm5.2"))] Rocm52, /// Use the PyTorch index for ROCm 5.1.1. #[serde(rename = "rocm5.1.1")] - #[clap(name = "rocm5.1.1")] + #[cfg_attr(feature = "clap", clap(name = "rocm5.1.1"))] Rocm511, /// Use the PyTorch index for ROCm 4.2. #[serde(rename = "rocm4.2")] - #[clap(name = "rocm4.2")] + #[cfg_attr(feature = "clap", clap(name = "rocm4.2"))] Rocm42, /// Use the PyTorch index for ROCm 4.1. #[serde(rename = "rocm4.1")] - #[clap(name = "rocm4.1")] + #[cfg_attr(feature = "clap", clap(name = "rocm4.1"))] Rocm41, /// Use the PyTorch index for ROCm 4.0.1. #[serde(rename = "rocm4.0.1")] - #[clap(name = "rocm4.0.1")] + #[cfg_attr(feature = "clap", clap(name = "rocm4.0.1"))] Rocm401, } From 75d4cd30d6bbb13e2d871d614abe2065c46e9b4a Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Wed, 18 Jun 2025 09:55:09 -0500 Subject: [PATCH 046/185] Use Depot for Windows `cargo test` (#14122) Replaces https://github.com/astral-sh/uv/pull/12320 Switches to Depot for the large Windows runner we use for `cargo test`. The runtime goes from 8m 20s -> 6m 44s (total) and 7m 18s -> 4m 41s (test run) which are 20% and 35% speedups respectively. A few things got marginally slower, like Python installs went from 11s -> 38s, the Rust cache went from 15s -> 30s, and drive setup went from 7s -> 20s. --- .github/workflows/ci.yml | 2 +- .github/workflows/setup-dev-drive.ps1 | 45 ++++++++++++++++++++++----- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index feaa38210..459495600 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -266,7 +266,7 @@ jobs: timeout-minutes: 15 needs: determine_changes if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }} - runs-on: github-windows-2025-x86_64-16 + runs-on: depot-windows-2022-16 name: "cargo test | windows" steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 diff --git a/.github/workflows/setup-dev-drive.ps1 b/.github/workflows/setup-dev-drive.ps1 index e0e2a765b..e003cc359 100644 --- a/.github/workflows/setup-dev-drive.ps1 +++ b/.github/workflows/setup-dev-drive.ps1 @@ -1,13 +1,43 @@ # Configures a drive for testing in CI. +# +# When using standard GitHub Actions runners, a `D:` drive is present and has +# similar or better performance characteristics than a ReFS dev drive. Sometimes +# using a larger runner is still more performant (e.g., when running the test +# suite) and we need to create a dev drive. This script automatically configures +# the appropriate drive. +# +# When using GitHub Actions' "larger runners", the `D:` drive is not present and +# we create a DevDrive mount on `C:`. This is purported to be more performant +# than an ReFS drive, though we did not see a change when we switched over. +# +# When using Depot runners, the underling infrastructure is EC2, which does not +# support Hyper-V. The `New-VHD` commandlet only works with Hyper-V, but we can +# create a ReFS drive using `diskpart` and `format` directory. We cannot use a +# DevDrive, as that also requires Hyper-V. The Depot runners use `D:` already, +# so we must check if it's a Depot runner first, and we use `V:` as the target +# instead. -# When not using a GitHub Actions "larger runner", the `D:` drive is present and -# has similar or better performance characteristics than a ReFS dev drive. -# Sometimes using a larger runner is still more performant (e.g., when running -# the test suite) and we need to create a dev drive. This script automatically -# configures the appropriate drive. -# Note we use `Get-PSDrive` is not sufficient because the drive letter is assigned. -if (Test-Path "D:\") { +if ($env:DEPOT_RUNNER -eq "1") { + Write-Output "DEPOT_RUNNER detected, setting up custom dev drive..." + + # Create VHD and configure drive using diskpart + $vhdPath = "C:\uv_dev_drive.vhdx" + @" +create vdisk file="$vhdPath" maximum=20480 type=expandable +attach vdisk +create partition primary +active +assign letter=V +"@ | diskpart + + # Format the drive as ReFS + format V: /fs:ReFS /q /y + $Drive = "V:" + + Write-Output "Custom dev drive created at $Drive" +} elseif (Test-Path "D:\") { + # Note `Get-PSDrive` is not sufficient because the drive letter is assigned. Write-Output "Using existing drive at D:" $Drive = "D:" } else { @@ -61,4 +91,3 @@ Write-Output ` "UV_WORKSPACE=$($Drive)/uv" ` "PATH=$($Drive)/.cargo/bin;$env:PATH" ` >> $env:GITHUB_ENV - From 1fc65a1d9de51b2dd567733208f4595dc442db89 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Wed, 18 Jun 2025 11:30:37 -0500 Subject: [PATCH 047/185] Publish to DockerHub (#14088) The primary motivation here is to avoid confusion with non-official repositories, e.g., https://github.com/astral-sh/uv/issues/13958 which could lead to attacks against our users. Resolves - https://github.com/astral-sh/uv/issues/12679 - #8699 --- .github/workflows/build-docker.yml | 34 ++++++++++++++++++------------ 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml index e310add68..4f19fb3df 100644 --- a/.github/workflows/build-docker.yml +++ b/.github/workflows/build-docker.yml @@ -37,7 +37,8 @@ on: - .github/workflows/build-docker.yml env: - UV_BASE_IMG: ghcr.io/${{ github.repository_owner }}/uv + UV_GHCR_IMAGE: ghcr.io/${{ github.repository_owner }}/uv + UV_DOCKERHUB_IMAGE: docker.io/astral/uv jobs: docker-plan: @@ -84,13 +85,12 @@ jobs: with: submodules: recursive - # Login to DockerHub first, to avoid rate-limiting + # Login to DockerHub (when not pushing, it's to avoid rate-limiting) - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 - # PRs from forks don't have access to secrets, disable this step in that case. if: ${{ github.event.pull_request.head.repo.full_name == 'astral-sh/uv' }} with: - username: astralshbot - password: ${{ secrets.DOCKERHUB_TOKEN_RO }} + username: ${{ needs.docker-plan.outputs.push == 'true' && 'astral' || 'astralshbot' }} + password: ${{ needs.docker-plan.outputs.push == 'true' && secrets.DOCKERHUB_TOKEN_RW || secrets.DOCKERHUB_TOKEN_RO }} - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 with: @@ -117,7 +117,9 @@ jobs: id: meta uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0 with: - images: ${{ env.UV_BASE_IMG }} + images: | + ${{ env.UV_GHCR_IMAGE }} + ${{ env.UV_DOCKERHUB_IMAGE }} # Defining this makes sure the org.opencontainers.image.version OCI label becomes the actual release version and not the branch name tags: | type=raw,value=dry-run,enable=${{ needs.docker-plan.outputs.push == 'false' }} @@ -186,12 +188,12 @@ jobs: - python:3.9-slim-bookworm,python3.9-bookworm-slim - python:3.8-slim-bookworm,python3.8-bookworm-slim steps: - # Login to DockerHub first, to avoid rate-limiting + # Login to DockerHub (when not pushing, it's to avoid rate-limiting) - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 if: ${{ github.event.pull_request.head.repo.full_name == 'astral-sh/uv' }} with: - username: astralshbot - password: ${{ secrets.DOCKERHUB_TOKEN_RO }} + username: ${{ needs.docker-plan.outputs.push == 'true' && 'astral' || 'astralshbot' }} + password: ${{ needs.docker-plan.outputs.push == 'true' && secrets.DOCKERHUB_TOKEN_RW || secrets.DOCKERHUB_TOKEN_RO }} - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 with: @@ -212,7 +214,7 @@ jobs: # Generate Dockerfile content cat < Dockerfile FROM ${BASE_IMAGE} - COPY --from=${{ env.UV_BASE_IMG }}:latest /uv /uvx /usr/local/bin/ + COPY --from=${{ env.UV_GHCR_IMAGE }}:latest /uv /uvx /usr/local/bin/ ENTRYPOINT [] CMD ["/usr/local/bin/uv"] EOF @@ -245,7 +247,9 @@ jobs: env: DOCKER_METADATA_ANNOTATIONS_LEVELS: index with: - images: ${{ env.UV_BASE_IMG }} + images: | + ${{ env.UV_GHCR_IMAGE }} + ${{ env.UV_DOCKERHUB_IMAGE }} flavor: | latest=false tags: | @@ -266,7 +270,7 @@ jobs: - name: Generate artifact attestation uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0 with: - subject-name: ${{ env.UV_BASE_IMG }} + subject-name: ${{ env.UV_GHCR_IMAGE }} subject-digest: ${{ steps.build-and-push.outputs.digest }} # Re-tag the base image, to ensure it's shown as the newest on the registry UI @@ -289,12 +293,16 @@ jobs: - name: Push tags env: - IMAGE: ${{ env.UV_BASE_IMG }} + IMAGE: ${{ env.UV_GHCR_IMAGE }} DIGEST: ${{ needs.docker-publish-base.outputs.image-digest }} TAGS: ${{ needs.docker-publish-base.outputs.image-tags }} run: | docker pull "${IMAGE}@${DIGEST}" for tag in $TAGS; do + # Skip re-tag for DockerHub + if [[ "$tag" == "${{ env.UV_DOCKERHUB_IMAGE }}"* ]]; then + continue + fi docker tag "${IMAGE}@${DIGEST}" "${tag}" docker push "${tag}" done From e1046242e7449dabd6af61df26187e1848c3ec55 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Wed, 18 Jun 2025 13:46:30 -0500 Subject: [PATCH 048/185] Fix Docker attestations (#14133) These regressed in #14088 and were found during my test publish from a fork. --- .github/workflows/build-docker.yml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml index 4f19fb3df..1f5229aef 100644 --- a/.github/workflows/build-docker.yml +++ b/.github/workflows/build-docker.yml @@ -73,8 +73,9 @@ jobs: runs-on: ubuntu-latest permissions: contents: read - id-token: write # for Depot OIDC - packages: write # for GHCR + id-token: write # for Depot OIDC and GHCR signing + packages: write # for GHCR image pushes + attestations: write # for GHCR attestations environment: name: release outputs: @@ -141,7 +142,7 @@ jobs: if: ${{ needs.docker-plan.outputs.push == 'true' }} uses: actions/attest-build-provenance@c074443f1aee8d4aeeae555aebba3282517141b2 # v2.2.3 with: - subject-name: ${{ env.UV_BASE_IMG }} + subject-name: ${{ env.UV_GHCR_IMAGE }} subject-digest: ${{ steps.build.outputs.digest }} docker-publish-extra: @@ -154,9 +155,9 @@ jobs: - docker-publish-base if: ${{ needs.docker-plan.outputs.push == 'true' }} permissions: - packages: write - attestations: write # needed to push image attestations to the Github attestation store - id-token: write # needed for signing the images with GitHub OIDC Token + id-token: write # for Depot OIDC and GHCR signing + packages: write # for GHCR image pushes + attestations: write # for GHCR attestations strategy: fail-fast: false matrix: From c3e4b6380608a0425982f3082a5403e7249a7532 Mon Sep 17 00:00:00 2001 From: Jack O'Connor Date: Wed, 18 Jun 2025 12:12:56 -0700 Subject: [PATCH 049/185] document the way member sources shadow workspace sources Closes https://github.com/astral-sh/uv/issues/14093. --- docs/concepts/projects/workspaces.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/concepts/projects/workspaces.md b/docs/concepts/projects/workspaces.md index 942cea8c2..4b2d670b4 100644 --- a/docs/concepts/projects/workspaces.md +++ b/docs/concepts/projects/workspaces.md @@ -113,6 +113,13 @@ build-backend = "hatchling.build" Every workspace member would, by default, install `tqdm` from GitHub, unless a specific member overrides the `tqdm` entry in its own `tool.uv.sources` table. +!!! note + + If a workspace member provides `tool.uv.sources` for some dependency, it will ignore any + `tool.uv.sources` for the same dependency in the workspace root, even if the member's source is + limited by a [marker](dependencies.md#platform-specific-sources) that doesn't match the current + platform. + ## Workspace layouts The most common workspace layout can be thought of as a root project with a series of accompanying From cc8d5a92154717f5c6baccb21b45cc1cd0dccca1 Mon Sep 17 00:00:00 2001 From: Jack O'Connor Date: Thu, 19 Jun 2025 14:47:22 -0700 Subject: [PATCH 050/185] handle an existing shebang in `uv init --script` (#14141) Closes https://github.com/astral-sh/uv/issues/14085. --- Cargo.lock | 2 ++ crates/uv-scripts/Cargo.toml | 2 ++ crates/uv-scripts/src/lib.rs | 23 +++++++++++--- crates/uv-warnings/src/lib.rs | 8 ++--- crates/uv/tests/it/init.rs | 59 +++++++++++++++++++++++++++++++++++ docs/guides/scripts.md | 2 ++ 6 files changed, 88 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 264a17e55..77a17a4f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5742,6 +5742,7 @@ dependencies = [ "fs-err 3.1.1", "indoc", "memchr", + "regex", "serde", "thiserror 2.0.12", "toml", @@ -5751,6 +5752,7 @@ dependencies = [ "uv-pypi-types", "uv-redacted", "uv-settings", + "uv-warnings", "uv-workspace", ] diff --git a/crates/uv-scripts/Cargo.toml b/crates/uv-scripts/Cargo.toml index 993633918..124eb1fea 100644 --- a/crates/uv-scripts/Cargo.toml +++ b/crates/uv-scripts/Cargo.toml @@ -16,11 +16,13 @@ uv-pep508 = { workspace = true } uv-pypi-types = { workspace = true } uv-redacted = { workspace = true } uv-settings = { workspace = true } +uv-warnings = { workspace = true } uv-workspace = { workspace = true } fs-err = { workspace = true, features = ["tokio"] } indoc = { workspace = true } memchr = { workspace = true } +regex = { workspace = true } serde = { workspace = true, features = ["derive"] } thiserror = { workspace = true } toml = { workspace = true } diff --git a/crates/uv-scripts/src/lib.rs b/crates/uv-scripts/src/lib.rs index 1023b4141..b80cdc219 100644 --- a/crates/uv-scripts/src/lib.rs +++ b/crates/uv-scripts/src/lib.rs @@ -14,6 +14,7 @@ use uv_pep508::PackageName; use uv_pypi_types::VerbatimParsedUrl; use uv_redacted::DisplaySafeUrl; use uv_settings::{GlobalOptions, ResolverInstallerOptions}; +use uv_warnings::warn_user; use uv_workspace::pyproject::Sources; static FINDER: LazyLock = LazyLock::new(|| Finder::new(b"# /// script")); @@ -238,11 +239,25 @@ impl Pep723Script { let metadata = serialize_metadata(&default_metadata); let script = if let Some(existing_contents) = existing_contents { + let (mut shebang, contents) = extract_shebang(&existing_contents)?; + if !shebang.is_empty() { + shebang.push_str("\n#\n"); + // If the shebang doesn't contain `uv`, it's probably something like + // `#! /usr/bin/env python`, which isn't going to respect the inline metadata. + // Issue a warning for users who might not know that. + // TODO: There are a lot of mistakes we could consider detecting here, like + // `uv run` without `--script` when the file doesn't end in `.py`. + if !regex::Regex::new(r"\buv\b").unwrap().is_match(&shebang) { + warn_user!( + "If you execute {} directly, it might ignore its inline metadata.\nConsider replacing its shebang with: {}", + file.to_string_lossy().cyan(), + "#!/usr/bin/env -S uv run --script".cyan(), + ); + } + } indoc::formatdoc! {r" - {metadata} - {content} - ", - content = String::from_utf8(existing_contents).map_err(|err| Pep723Error::Utf8(err.utf8_error()))?} + {shebang}{metadata} + {contents}" } } else { indoc::formatdoc! {r#" {metadata} diff --git a/crates/uv-warnings/src/lib.rs b/crates/uv-warnings/src/lib.rs index 9ed4c646e..5f2287cac 100644 --- a/crates/uv-warnings/src/lib.rs +++ b/crates/uv-warnings/src/lib.rs @@ -24,7 +24,7 @@ pub fn disable() { /// Warn a user, if warnings are enabled. #[macro_export] macro_rules! warn_user { - ($($arg:tt)*) => { + ($($arg:tt)*) => {{ use $crate::anstream::eprintln; use $crate::owo_colors::OwoColorize; @@ -33,7 +33,7 @@ macro_rules! warn_user { let formatted = message.bold(); eprintln!("{}{} {formatted}", "warning".yellow().bold(), ":".bold()); } - }; + }}; } pub static WARNINGS: LazyLock>> = LazyLock::new(Mutex::default); @@ -42,7 +42,7 @@ pub static WARNINGS: LazyLock>> = LazyLock::new(Mutex::d /// message. #[macro_export] macro_rules! warn_user_once { - ($($arg:tt)*) => { + ($($arg:tt)*) => {{ use $crate::anstream::eprintln; use $crate::owo_colors::OwoColorize; @@ -54,5 +54,5 @@ macro_rules! warn_user_once { } } } - }; + }}; } diff --git a/crates/uv/tests/it/init.rs b/crates/uv/tests/it/init.rs index e9e5e54a7..c5993d670 100644 --- a/crates/uv/tests/it/init.rs +++ b/crates/uv/tests/it/init.rs @@ -929,6 +929,65 @@ fn init_script_file_conflicts() -> Result<()> { Ok(()) } +// Init script should not trash an existing shebang. +#[test] +fn init_script_shebang() -> Result<()> { + let context = TestContext::new("3.12"); + + let script_path = context.temp_dir.child("script.py"); + + let contents = "#! /usr/bin/env python3\nprint(\"Hello, world!\")"; + fs_err::write(&script_path, contents)?; + uv_snapshot!(context.filters(), context.init().arg("--script").arg("script.py"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: If you execute script.py directly, it might ignore its inline metadata. + Consider replacing its shebang with: #!/usr/bin/env -S uv run --script + Initialized script at `script.py` + "); + let resulting_script = fs_err::read_to_string(&script_path)?; + assert_snapshot!(resulting_script, @r#" + #! /usr/bin/env python3 + # + # /// script + # requires-python = ">=3.12" + # dependencies = [] + # /// + + print("Hello, world!") + "# + ); + + // If the shebang already contains `uv`, the result is the same, but we suppress the warning. + let contents = "#!/usr/bin/env -S uv run --script\nprint(\"Hello, world!\")"; + fs_err::write(&script_path, contents)?; + uv_snapshot!(context.filters(), context.init().arg("--script").arg("script.py"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Initialized script at `script.py` + "); + let resulting_script = fs_err::read_to_string(&script_path)?; + assert_snapshot!(resulting_script, @r#" + #!/usr/bin/env -S uv run --script + # + # /// script + # requires-python = ">=3.12" + # dependencies = [] + # /// + + print("Hello, world!") + "# + ); + + Ok(()) +} + /// Run `uv init --lib` with an existing py.typed file #[test] fn init_py_typed_exists() -> Result<()> { diff --git a/docs/guides/scripts.md b/docs/guides/scripts.md index 7142db155..26d85e76d 100644 --- a/docs/guides/scripts.md +++ b/docs/guides/scripts.md @@ -241,10 +241,12 @@ Declaration of dependencies is also supported in this context, for example: ```python title="example" #!/usr/bin/env -S uv run --script +# # /// script # requires-python = ">=3.12" # dependencies = ["httpx"] # /// + import httpx print(httpx.get("https://example.com")) From 62365d4ec86c146fc1d2050bff3fa70e53abfa08 Mon Sep 17 00:00:00 2001 From: John Mumm Date: Fri, 20 Jun 2025 03:21:32 -0400 Subject: [PATCH 051/185] Support netrc and same-origin credential propagation on index redirects (#14126) This PR is a combination of #12920 and #13754. Prior to these changes, following a redirect when searching indexes would bypass our authentication middleware. This PR updates uv to support propagating credentials through our middleware on same-origin redirects and to support netrc credentials for both same- and cross-origin redirects. It does not handle the case described in #11097 where the redirect location itself includes credentials (e.g., `https://user:pass@redirect-location.com`). That will be addressed in follow-up work. This includes unit tests for the new redirect logic and integration tests for credential propagation. The automated external registries test is also passing for AWS CodeArtifact, Azure Artifacts, GCP Artifact Registry, JFrog Artifactory, GitLab, Cloudsmith, and Gemfury. --- Cargo.lock | 1 + crates/uv-client/Cargo.toml | 1 + crates/uv-client/src/base_client.rs | 574 ++++++++++++++++++++++- crates/uv-client/src/cached_client.rs | 2 - crates/uv-client/src/lib.rs | 2 +- crates/uv-client/src/registry_client.rs | 243 +++++++++- crates/uv-distribution/src/source/mod.rs | 7 +- crates/uv-git/src/resolver.rs | 4 +- crates/uv-publish/src/lib.rs | 27 +- crates/uv/tests/it/common/mod.rs | 4 +- crates/uv/tests/it/edit.rs | 191 ++++++++ 11 files changed, 1022 insertions(+), 34 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 77a17a4f3..a30a0cbe1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4946,6 +4946,7 @@ dependencies = [ "uv-torch", "uv-version", "uv-warnings", + "wiremock", ] [[package]] diff --git a/crates/uv-client/Cargo.toml b/crates/uv-client/Cargo.toml index 81d1909fe..bc7fc611f 100644 --- a/crates/uv-client/Cargo.toml +++ b/crates/uv-client/Cargo.toml @@ -65,3 +65,4 @@ hyper = { version = "1.4.1", features = ["server", "http1"] } hyper-util = { version = "0.1.8", features = ["tokio"] } insta = { version = "1.40.0", features = ["filters", "json", "redactions"] } tokio = { workspace = true } +wiremock = { workspace = true } diff --git a/crates/uv-client/src/base_client.rs b/crates/uv-client/src/base_client.rs index f5fda246d..85c384b0d 100644 --- a/crates/uv-client/src/base_client.rs +++ b/crates/uv-client/src/base_client.rs @@ -6,14 +6,23 @@ use std::sync::Arc; use std::time::Duration; use std::{env, io, iter}; +use anyhow::anyhow; +use http::{ + HeaderMap, HeaderName, HeaderValue, Method, StatusCode, + header::{ + AUTHORIZATION, CONTENT_ENCODING, CONTENT_LENGTH, CONTENT_TYPE, COOKIE, LOCATION, + PROXY_AUTHORIZATION, REFERER, TRANSFER_ENCODING, WWW_AUTHENTICATE, + }, +}; use itertools::Itertools; -use reqwest::{Client, ClientBuilder, Proxy, Response}; +use reqwest::{Client, ClientBuilder, IntoUrl, Proxy, Request, Response, multipart}; use reqwest_middleware::{ClientWithMiddleware, Middleware}; use reqwest_retry::policies::ExponentialBackoff; use reqwest_retry::{ DefaultRetryableStrategy, RetryTransientMiddleware, Retryable, RetryableStrategy, }; use tracing::{debug, trace}; +use url::ParseError; use url::Url; use uv_auth::{AuthMiddleware, Indexes}; @@ -32,6 +41,10 @@ use crate::middleware::OfflineMiddleware; use crate::tls::read_identity; pub const DEFAULT_RETRIES: u32 = 3; +/// Maximum number of redirects to follow before giving up. +/// +/// This is the default used by [`reqwest`]. +const DEFAULT_MAX_REDIRECTS: u32 = 10; /// Selectively skip parts or the entire auth middleware. #[derive(Debug, Clone, Copy, Default)] @@ -61,6 +74,31 @@ pub struct BaseClientBuilder<'a> { default_timeout: Duration, extra_middleware: Option, proxies: Vec, + redirect_policy: RedirectPolicy, + /// Whether credentials should be propagated during cross-origin redirects. + /// + /// A policy allowing propagation is insecure and should only be available for test code. + cross_origin_credential_policy: CrossOriginCredentialsPolicy, +} + +/// The policy for handling HTTP redirects. +#[derive(Debug, Default, Clone, Copy)] +pub enum RedirectPolicy { + /// Use reqwest's built-in redirect handling. This bypasses our custom middleware + /// on redirect. + #[default] + BypassMiddleware, + /// Handle redirects manually, re-triggering our custom middleware for each request. + RetriggerMiddleware, +} + +impl RedirectPolicy { + pub fn reqwest_policy(self) -> reqwest::redirect::Policy { + match self { + RedirectPolicy::BypassMiddleware => reqwest::redirect::Policy::default(), + RedirectPolicy::RetriggerMiddleware => reqwest::redirect::Policy::none(), + } + } } /// A list of user-defined middlewares to be applied to the client. @@ -96,6 +134,8 @@ impl BaseClientBuilder<'_> { default_timeout: Duration::from_secs(30), extra_middleware: None, proxies: vec![], + redirect_policy: RedirectPolicy::default(), + cross_origin_credential_policy: CrossOriginCredentialsPolicy::Secure, } } } @@ -173,6 +213,24 @@ impl<'a> BaseClientBuilder<'a> { self } + #[must_use] + pub fn redirect(mut self, policy: RedirectPolicy) -> Self { + self.redirect_policy = policy; + self + } + + /// Allows credentials to be propagated on cross-origin redirects. + /// + /// WARNING: This should only be available for tests. In production code, propagating credentials + /// during cross-origin redirects can lead to security vulnerabilities including credential + /// leakage to untrusted domains. + #[cfg(test)] + #[must_use] + pub fn allow_cross_origin_credentials(mut self) -> Self { + self.cross_origin_credential_policy = CrossOriginCredentialsPolicy::Insecure; + self + } + pub fn is_offline(&self) -> bool { matches!(self.connectivity, Connectivity::Offline) } @@ -229,6 +287,7 @@ impl<'a> BaseClientBuilder<'a> { timeout, ssl_cert_file_exists, Security::Secure, + self.redirect_policy, ); // Create an insecure client that accepts invalid certificates. @@ -237,11 +296,20 @@ impl<'a> BaseClientBuilder<'a> { timeout, ssl_cert_file_exists, Security::Insecure, + self.redirect_policy, ); // Wrap in any relevant middleware and handle connectivity. - let client = self.apply_middleware(raw_client.clone()); - let dangerous_client = self.apply_middleware(raw_dangerous_client.clone()); + let client = RedirectClientWithMiddleware { + client: self.apply_middleware(raw_client.clone()), + redirect_policy: self.redirect_policy, + cross_origin_credentials_policy: self.cross_origin_credential_policy, + }; + let dangerous_client = RedirectClientWithMiddleware { + client: self.apply_middleware(raw_dangerous_client.clone()), + redirect_policy: self.redirect_policy, + cross_origin_credentials_policy: self.cross_origin_credential_policy, + }; BaseClient { connectivity: self.connectivity, @@ -258,8 +326,16 @@ impl<'a> BaseClientBuilder<'a> { /// Share the underlying client between two different middleware configurations. pub fn wrap_existing(&self, existing: &BaseClient) -> BaseClient { // Wrap in any relevant middleware and handle connectivity. - let client = self.apply_middleware(existing.raw_client.clone()); - let dangerous_client = self.apply_middleware(existing.raw_dangerous_client.clone()); + let client = RedirectClientWithMiddleware { + client: self.apply_middleware(existing.raw_client.clone()), + redirect_policy: self.redirect_policy, + cross_origin_credentials_policy: self.cross_origin_credential_policy, + }; + let dangerous_client = RedirectClientWithMiddleware { + client: self.apply_middleware(existing.raw_dangerous_client.clone()), + redirect_policy: self.redirect_policy, + cross_origin_credentials_policy: self.cross_origin_credential_policy, + }; BaseClient { connectivity: self.connectivity, @@ -279,6 +355,7 @@ impl<'a> BaseClientBuilder<'a> { timeout: Duration, ssl_cert_file_exists: bool, security: Security, + redirect_policy: RedirectPolicy, ) -> Client { // Configure the builder. let client_builder = ClientBuilder::new() @@ -286,7 +363,8 @@ impl<'a> BaseClientBuilder<'a> { .user_agent(user_agent) .pool_max_idle_per_host(20) .read_timeout(timeout) - .tls_built_in_root_certs(false); + .tls_built_in_root_certs(false) + .redirect(redirect_policy.reqwest_policy()); // If necessary, accept invalid certificates. let client_builder = match security { @@ -381,9 +459,9 @@ impl<'a> BaseClientBuilder<'a> { #[derive(Debug, Clone)] pub struct BaseClient { /// The underlying HTTP client that enforces valid certificates. - client: ClientWithMiddleware, + client: RedirectClientWithMiddleware, /// The underlying HTTP client that accepts invalid certificates. - dangerous_client: ClientWithMiddleware, + dangerous_client: RedirectClientWithMiddleware, /// The HTTP client without middleware. raw_client: Client, /// The HTTP client that accepts invalid certificates without middleware. @@ -408,7 +486,7 @@ enum Security { impl BaseClient { /// Selects the appropriate client based on the host's trustworthiness. - pub fn for_host(&self, url: &DisplaySafeUrl) -> &ClientWithMiddleware { + pub fn for_host(&self, url: &DisplaySafeUrl) -> &RedirectClientWithMiddleware { if self.disable_ssl(url) { &self.dangerous_client } else { @@ -416,6 +494,12 @@ impl BaseClient { } } + /// Executes a request, applying redirect policy. + pub async fn execute(&self, req: Request) -> reqwest_middleware::Result { + let client = self.for_host(&DisplaySafeUrl::from(req.url().clone())); + client.execute(req).await + } + /// Returns `true` if the host is trusted to use the insecure client. pub fn disable_ssl(&self, url: &DisplaySafeUrl) -> bool { self.allow_insecure_host @@ -439,6 +523,316 @@ impl BaseClient { } } +/// Wrapper around [`ClientWithMiddleware`] that manages redirects. +#[derive(Debug, Clone)] +pub struct RedirectClientWithMiddleware { + client: ClientWithMiddleware, + redirect_policy: RedirectPolicy, + /// Whether credentials should be preserved during cross-origin redirects. + /// + /// WARNING: This should only be available for tests. In production code, preserving credentials + /// during cross-origin redirects can lead to security vulnerabilities including credential + /// leakage to untrusted domains. + cross_origin_credentials_policy: CrossOriginCredentialsPolicy, +} + +impl RedirectClientWithMiddleware { + /// Convenience method to make a `GET` request to a URL. + pub fn get(&self, url: U) -> RequestBuilder { + RequestBuilder::new(self.client.get(url), self) + } + + /// Convenience method to make a `POST` request to a URL. + pub fn post(&self, url: U) -> RequestBuilder { + RequestBuilder::new(self.client.post(url), self) + } + + /// Convenience method to make a `HEAD` request to a URL. + pub fn head(&self, url: U) -> RequestBuilder { + RequestBuilder::new(self.client.head(url), self) + } + + /// Executes a request, applying the redirect policy. + pub async fn execute(&self, req: Request) -> reqwest_middleware::Result { + match self.redirect_policy { + RedirectPolicy::BypassMiddleware => self.client.execute(req).await, + RedirectPolicy::RetriggerMiddleware => self.execute_with_redirect_handling(req).await, + } + } + + /// Executes a request. If the response is a redirect (one of HTTP 301, 302, 303, 307, or 308), the + /// request is executed again with the redirect location URL (up to a maximum number of + /// redirects). + /// + /// Unlike the built-in reqwest redirect policies, this sends the redirect request through the + /// entire middleware pipeline again. + /// + /// See RFC 7231 7.1.2 for details on + /// redirect semantics. + async fn execute_with_redirect_handling( + &self, + req: Request, + ) -> reqwest_middleware::Result { + let mut request = req; + let mut redirects = 0; + let max_redirects = DEFAULT_MAX_REDIRECTS; + + loop { + let result = self + .client + .execute(request.try_clone().expect("HTTP request must be cloneable")) + .await; + let Ok(response) = result else { + return result; + }; + + if redirects >= max_redirects { + return Ok(response); + } + + let Some(redirect_request) = + request_into_redirect(request, &response, self.cross_origin_credentials_policy)? + else { + return Ok(response); + }; + + redirects += 1; + request = redirect_request; + } + } + + pub fn raw_client(&self) -> &ClientWithMiddleware { + &self.client + } +} + +impl From for ClientWithMiddleware { + fn from(item: RedirectClientWithMiddleware) -> ClientWithMiddleware { + item.client + } +} + +/// Check if this is should be a redirect and, if so, return a new redirect request. +/// +/// This implementation is based on the [`reqwest`] crate redirect implementation. +/// It takes ownership of the original [`Request`] and mutates it to create the new +/// redirect [`Request`]. +fn request_into_redirect( + mut req: Request, + res: &Response, + cross_origin_credentials_policy: CrossOriginCredentialsPolicy, +) -> reqwest_middleware::Result> { + let original_req_url = DisplaySafeUrl::from(req.url().clone()); + let status = res.status(); + let should_redirect = match status { + StatusCode::MOVED_PERMANENTLY + | StatusCode::FOUND + | StatusCode::TEMPORARY_REDIRECT + | StatusCode::PERMANENT_REDIRECT => true, + StatusCode::SEE_OTHER => { + // Per RFC 7231, HTTP 303 is intended for the user agent + // to perform a GET or HEAD request to the redirect target. + // Historically, some browsers also changed method from POST + // to GET on 301 or 302, but this is not required by RFC 7231 + // and was not intended by the HTTP spec. + *req.body_mut() = None; + for header in &[ + TRANSFER_ENCODING, + CONTENT_ENCODING, + CONTENT_TYPE, + CONTENT_LENGTH, + ] { + req.headers_mut().remove(header); + } + + match *req.method() { + Method::GET | Method::HEAD => {} + _ => { + *req.method_mut() = Method::GET; + } + } + true + } + _ => false, + }; + if !should_redirect { + return Ok(None); + } + + let location = res + .headers() + .get(LOCATION) + .ok_or(reqwest_middleware::Error::Middleware(anyhow!( + "Server returned redirect (HTTP {status}) without destination URL. This may indicate a server configuration issue" + )))? + .to_str() + .map_err(|_| { + reqwest_middleware::Error::Middleware(anyhow!( + "Invalid HTTP {status} 'Location' value: must only contain visible ascii characters" + )) + })?; + + let mut redirect_url = match DisplaySafeUrl::parse(location) { + Ok(url) => url, + // Per RFC 7231, URLs should be resolved against the request URL. + Err(ParseError::RelativeUrlWithoutBase) => original_req_url.join(location).map_err(|err| { + reqwest_middleware::Error::Middleware(anyhow!( + "Invalid HTTP {status} 'Location' value `{location}` relative to `{original_req_url}`: {err}" + )) + })?, + Err(err) => { + return Err(reqwest_middleware::Error::Middleware(anyhow!( + "Invalid HTTP {status} 'Location' value `{location}`: {err}" + ))); + } + }; + // Per RFC 7231, fragments must be propagated + if let Some(fragment) = original_req_url.fragment() { + redirect_url.set_fragment(Some(fragment)); + } + + // Ensure the URL is a valid HTTP URI. + if let Err(err) = redirect_url.as_str().parse::() { + return Err(reqwest_middleware::Error::Middleware(anyhow!( + "HTTP {status} 'Location' value `{redirect_url}` is not a valid HTTP URI: {err}" + ))); + } + + if redirect_url.scheme() != "http" && redirect_url.scheme() != "https" { + return Err(reqwest_middleware::Error::Middleware(anyhow!( + "Invalid HTTP {status} 'Location' value `{redirect_url}`: scheme needs to be https or http" + ))); + } + + let mut headers = HeaderMap::new(); + std::mem::swap(req.headers_mut(), &mut headers); + + let cross_host = redirect_url.host_str() != original_req_url.host_str() + || redirect_url.port_or_known_default() != original_req_url.port_or_known_default(); + if cross_host { + if cross_origin_credentials_policy == CrossOriginCredentialsPolicy::Secure { + debug!("Received a cross-origin redirect. Removing sensitive headers."); + headers.remove(AUTHORIZATION); + headers.remove(COOKIE); + headers.remove(PROXY_AUTHORIZATION); + headers.remove(WWW_AUTHENTICATE); + } + // If the redirect request is not a cross-origin request and the original request already + // had a Referer header, attempt to set the Referer header for the redirect request. + } else if headers.contains_key(REFERER) { + if let Some(referer) = make_referer(&redirect_url, &original_req_url) { + headers.insert(REFERER, referer); + } + } + + std::mem::swap(req.headers_mut(), &mut headers); + *req.url_mut() = Url::from(redirect_url); + debug!( + "Received HTTP {status}. Redirecting to {}", + DisplaySafeUrl::ref_cast(req.url()) + ); + Ok(Some(req)) +} + +/// Return a Referer [`HeaderValue`] according to RFC 7231. +/// +/// Return [`None`] if https has been downgraded in the redirect location. +fn make_referer( + redirect_url: &DisplaySafeUrl, + original_url: &DisplaySafeUrl, +) -> Option { + if redirect_url.scheme() == "http" && original_url.scheme() == "https" { + return None; + } + + let mut referer = original_url.clone(); + referer.remove_credentials(); + referer.set_fragment(None); + referer.as_str().parse().ok() +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] +pub(crate) enum CrossOriginCredentialsPolicy { + /// Do not propagate credentials on cross-origin requests. + #[default] + Secure, + + /// Propagate credentials on cross-origin requests. + /// + /// WARNING: This should only be available for tests. In production code, preserving credentials + /// during cross-origin redirects can lead to security vulnerabilities including credential + /// leakage to untrusted domains. + #[cfg(test)] + Insecure, +} + +/// A builder to construct the properties of a `Request`. +/// +/// This wraps [`reqwest_middleware::RequestBuilder`] to ensure that the [`BaseClient`] +/// redirect policy is respected if `send()` is called. +#[derive(Debug)] +#[must_use] +pub struct RequestBuilder<'a> { + builder: reqwest_middleware::RequestBuilder, + client: &'a RedirectClientWithMiddleware, +} + +impl<'a> RequestBuilder<'a> { + pub fn new( + builder: reqwest_middleware::RequestBuilder, + client: &'a RedirectClientWithMiddleware, + ) -> Self { + Self { builder, client } + } + + /// Add a `Header` to this Request. + pub fn header(mut self, key: K, value: V) -> Self + where + HeaderName: TryFrom, + >::Error: Into, + HeaderValue: TryFrom, + >::Error: Into, + { + self.builder = self.builder.header(key, value); + self + } + + /// Add a set of Headers to the existing ones on this Request. + /// + /// The headers will be merged in to any already set. + pub fn headers(mut self, headers: HeaderMap) -> Self { + self.builder = self.builder.headers(headers); + self + } + + #[cfg(not(target_arch = "wasm32"))] + pub fn version(mut self, version: reqwest::Version) -> Self { + self.builder = self.builder.version(version); + self + } + + #[cfg_attr(docsrs, doc(cfg(feature = "multipart")))] + pub fn multipart(mut self, multipart: multipart::Form) -> Self { + self.builder = self.builder.multipart(multipart); + self + } + + /// Build a `Request`. + pub fn build(self) -> reqwest::Result { + self.builder.build() + } + + /// Constructs the Request and sends it to the target URL, returning a + /// future Response. + pub async fn send(self) -> reqwest_middleware::Result { + self.client.execute(self.build()?).await + } + + pub fn raw_builder(&self) -> &reqwest_middleware::RequestBuilder { + &self.builder + } +} + /// Extends [`DefaultRetryableStrategy`], to log transient request failures and additional retry cases. pub struct UvRetryableStrategy; @@ -528,3 +922,165 @@ fn find_source(orig: &dyn Error) -> Option<&E> { fn find_sources(orig: &dyn Error) -> impl Iterator { iter::successors(find_source::(orig), |&err| find_source(err)) } + +#[cfg(test)] +mod tests { + use super::*; + use anyhow::Result; + + use reqwest::{Client, Method}; + use wiremock::matchers::method; + use wiremock::{Mock, MockServer, ResponseTemplate}; + + use crate::base_client::request_into_redirect; + + #[tokio::test] + async fn test_redirect_preserves_authorization_header_on_same_origin() -> Result<()> { + for status in &[301, 302, 303, 307, 308] { + let server = MockServer::start().await; + Mock::given(method("GET")) + .respond_with( + ResponseTemplate::new(*status) + .insert_header("location", format!("{}/redirect", server.uri())), + ) + .mount(&server) + .await; + + let request = Client::new() + .get(server.uri()) + .basic_auth("username", Some("password")) + .build() + .unwrap(); + + assert!(request.headers().contains_key(AUTHORIZATION)); + + let response = Client::builder() + .redirect(reqwest::redirect::Policy::none()) + .build() + .unwrap() + .execute(request.try_clone().unwrap()) + .await + .unwrap(); + + let redirect_request = + request_into_redirect(request, &response, CrossOriginCredentialsPolicy::Secure)? + .unwrap(); + assert!(redirect_request.headers().contains_key(AUTHORIZATION)); + } + + Ok(()) + } + + #[tokio::test] + async fn test_redirect_removes_authorization_header_on_cross_origin() -> Result<()> { + for status in &[301, 302, 303, 307, 308] { + let server = MockServer::start().await; + Mock::given(method("GET")) + .respond_with( + ResponseTemplate::new(*status) + .insert_header("location", "https://cross-origin.com/simple"), + ) + .mount(&server) + .await; + + let request = Client::new() + .get(server.uri()) + .basic_auth("username", Some("password")) + .build() + .unwrap(); + + assert!(request.headers().contains_key(AUTHORIZATION)); + + let response = Client::builder() + .redirect(reqwest::redirect::Policy::none()) + .build() + .unwrap() + .execute(request.try_clone().unwrap()) + .await + .unwrap(); + + let redirect_request = + request_into_redirect(request, &response, CrossOriginCredentialsPolicy::Secure)? + .unwrap(); + assert!(!redirect_request.headers().contains_key(AUTHORIZATION)); + } + + Ok(()) + } + + #[tokio::test] + async fn test_redirect_303_changes_post_to_get() -> Result<()> { + let server = MockServer::start().await; + Mock::given(method("POST")) + .respond_with( + ResponseTemplate::new(303) + .insert_header("location", format!("{}/redirect", server.uri())), + ) + .mount(&server) + .await; + + let request = Client::new() + .post(server.uri()) + .basic_auth("username", Some("password")) + .build() + .unwrap(); + + assert_eq!(request.method(), Method::POST); + + let response = Client::builder() + .redirect(reqwest::redirect::Policy::none()) + .build() + .unwrap() + .execute(request.try_clone().unwrap()) + .await + .unwrap(); + + let redirect_request = + request_into_redirect(request, &response, CrossOriginCredentialsPolicy::Secure)? + .unwrap(); + assert_eq!(redirect_request.method(), Method::GET); + + Ok(()) + } + + #[tokio::test] + async fn test_redirect_no_referer_if_disabled() -> Result<()> { + for status in &[301, 302, 303, 307, 308] { + let server = MockServer::start().await; + Mock::given(method("GET")) + .respond_with( + ResponseTemplate::new(*status) + .insert_header("location", format!("{}/redirect", server.uri())), + ) + .mount(&server) + .await; + + let request = Client::builder() + .referer(false) + .build() + .unwrap() + .get(server.uri()) + .basic_auth("username", Some("password")) + .build() + .unwrap(); + + assert!(!request.headers().contains_key(REFERER)); + + let response = Client::builder() + .redirect(reqwest::redirect::Policy::none()) + .build() + .unwrap() + .execute(request.try_clone().unwrap()) + .await + .unwrap(); + + let redirect_request = + request_into_redirect(request, &response, CrossOriginCredentialsPolicy::Secure)? + .unwrap(); + + assert!(!redirect_request.headers().contains_key(REFERER)); + } + + Ok(()) + } +} diff --git a/crates/uv-client/src/cached_client.rs b/crates/uv-client/src/cached_client.rs index d19f95ec7..ee3314d1c 100644 --- a/crates/uv-client/src/cached_client.rs +++ b/crates/uv-client/src/cached_client.rs @@ -523,7 +523,6 @@ impl CachedClient { debug!("Sending revalidation request for: {url}"); let response = self .0 - .for_host(&url) .execute(req) .instrument(info_span!("revalidation_request", url = url.as_str())) .await @@ -564,7 +563,6 @@ impl CachedClient { let cache_policy_builder = CachePolicyBuilder::new(&req); let response = self .0 - .for_host(&url) .execute(req) .await .map_err(|err| ErrorKind::from_reqwest_middleware(url.clone(), err))?; diff --git a/crates/uv-client/src/lib.rs b/crates/uv-client/src/lib.rs index 3ea33204c..e42c86620 100644 --- a/crates/uv-client/src/lib.rs +++ b/crates/uv-client/src/lib.rs @@ -1,6 +1,6 @@ pub use base_client::{ AuthIntegration, BaseClient, BaseClientBuilder, DEFAULT_RETRIES, ExtraMiddleware, - UvRetryableStrategy, is_extended_transient_error, + RedirectClientWithMiddleware, RequestBuilder, UvRetryableStrategy, is_extended_transient_error, }; pub use cached_client::{CacheControl, CachedClient, CachedClientError, DataWithCachePolicy}; pub use error::{Error, ErrorKind, WrappedReqwestError}; diff --git a/crates/uv-client/src/registry_client.rs b/crates/uv-client/src/registry_client.rs index 6271b7d20..b53e1ed9a 100644 --- a/crates/uv-client/src/registry_client.rs +++ b/crates/uv-client/src/registry_client.rs @@ -10,7 +10,6 @@ use futures::{FutureExt, StreamExt, TryStreamExt}; use http::{HeaderMap, StatusCode}; use itertools::Either; use reqwest::{Proxy, Response}; -use reqwest_middleware::ClientWithMiddleware; use rustc_hash::FxHashMap; use tokio::sync::{Mutex, Semaphore}; use tracing::{Instrument, debug, info_span, instrument, trace, warn}; @@ -35,13 +34,16 @@ use uv_redacted::DisplaySafeUrl; use uv_small_str::SmallString; use uv_torch::TorchStrategy; -use crate::base_client::{BaseClientBuilder, ExtraMiddleware}; +use crate::base_client::{BaseClientBuilder, ExtraMiddleware, RedirectPolicy}; use crate::cached_client::CacheControl; use crate::flat_index::FlatIndexEntry; use crate::html::SimpleHtml; use crate::remote_metadata::wheel_metadata_from_remote_zip; use crate::rkyvutil::OwnedArchive; -use crate::{BaseClient, CachedClient, Error, ErrorKind, FlatIndexClient, FlatIndexEntries}; +use crate::{ + BaseClient, CachedClient, Error, ErrorKind, FlatIndexClient, FlatIndexEntries, + RedirectClientWithMiddleware, +}; /// A builder for an [`RegistryClient`]. #[derive(Debug, Clone)] @@ -149,9 +151,23 @@ impl<'a> RegistryClientBuilder<'a> { self } + /// Allows credentials to be propagated on cross-origin redirects. + /// + /// WARNING: This should only be available for tests. In production code, propagating credentials + /// during cross-origin redirects can lead to security vulnerabilities including credential + /// leakage to untrusted domains. + #[cfg(test)] + #[must_use] + pub fn allow_cross_origin_credentials(mut self) -> Self { + self.base_client_builder = self.base_client_builder.allow_cross_origin_credentials(); + self + } + pub fn build(self) -> RegistryClient { // Build a base client - let builder = self.base_client_builder; + let builder = self + .base_client_builder + .redirect(RedirectPolicy::RetriggerMiddleware); let client = builder.build(); @@ -248,7 +264,7 @@ impl RegistryClient { } /// Return the [`BaseClient`] used by this client. - pub fn uncached_client(&self, url: &DisplaySafeUrl) -> &ClientWithMiddleware { + pub fn uncached_client(&self, url: &DisplaySafeUrl) -> &RedirectClientWithMiddleware { self.client.uncached().for_host(url) } @@ -1215,12 +1231,229 @@ impl Connectivity { mod tests { use std::str::FromStr; + use url::Url; use uv_normalize::PackageName; use uv_pypi_types::{JoinRelativeError, SimpleJson}; use uv_redacted::DisplaySafeUrl; use crate::{SimpleMetadata, SimpleMetadatum, html::SimpleHtml}; + use uv_cache::Cache; + use wiremock::matchers::{basic_auth, method, path_regex}; + use wiremock::{Mock, MockServer, ResponseTemplate}; + + use crate::RegistryClientBuilder; + + type Error = Box; + + async fn start_test_server(username: &'static str, password: &'static str) -> MockServer { + let server = MockServer::start().await; + + Mock::given(method("GET")) + .and(basic_auth(username, password)) + .respond_with(ResponseTemplate::new(200)) + .mount(&server) + .await; + + Mock::given(method("GET")) + .respond_with(ResponseTemplate::new(401)) + .mount(&server) + .await; + + server + } + + #[tokio::test] + async fn test_redirect_to_server_with_credentials() -> Result<(), Error> { + let username = "user"; + let password = "password"; + + let auth_server = start_test_server(username, password).await; + let auth_base_url = DisplaySafeUrl::parse(&auth_server.uri())?; + + let redirect_server = MockServer::start().await; + + // Configure the redirect server to respond with a 302 to the auth server + Mock::given(method("GET")) + .respond_with( + ResponseTemplate::new(302).insert_header("Location", format!("{auth_base_url}")), + ) + .mount(&redirect_server) + .await; + + let redirect_server_url = DisplaySafeUrl::parse(&redirect_server.uri())?; + + let cache = Cache::temp()?; + let registry_client = RegistryClientBuilder::new(cache) + .allow_cross_origin_credentials() + .build(); + let client = registry_client.cached_client().uncached(); + + assert_eq!( + client + .for_host(&redirect_server_url) + .get(redirect_server.uri()) + .send() + .await? + .status(), + 401, + "Requests should fail if credentials are missing" + ); + + let mut url = redirect_server_url.clone(); + let _ = url.set_username(username); + let _ = url.set_password(Some(password)); + + assert_eq!( + client + .for_host(&redirect_server_url) + .get(Url::from(url)) + .send() + .await? + .status(), + 200, + "Requests should succeed if credentials are present" + ); + + Ok(()) + } + + #[tokio::test] + async fn test_redirect_root_relative_url() -> Result<(), Error> { + let username = "user"; + let password = "password"; + + let redirect_server = MockServer::start().await; + + // Configure the redirect server to respond with a 307 with a relative URL. + Mock::given(method("GET")) + .and(path_regex("/foo/")) + .respond_with( + ResponseTemplate::new(307).insert_header("Location", "/bar/baz/".to_string()), + ) + .mount(&redirect_server) + .await; + + Mock::given(method("GET")) + .and(path_regex("/bar/baz/")) + .and(basic_auth(username, password)) + .respond_with(ResponseTemplate::new(200)) + .mount(&redirect_server) + .await; + + let redirect_server_url = DisplaySafeUrl::parse(&redirect_server.uri())?.join("foo/")?; + + let cache = Cache::temp()?; + let registry_client = RegistryClientBuilder::new(cache) + .allow_cross_origin_credentials() + .build(); + let client = registry_client.cached_client().uncached(); + + let mut url = redirect_server_url.clone(); + let _ = url.set_username(username); + let _ = url.set_password(Some(password)); + + assert_eq!( + client + .for_host(&url) + .get(Url::from(url)) + .send() + .await? + .status(), + 200, + "Requests should succeed for relative URL" + ); + + Ok(()) + } + + #[tokio::test] + async fn test_redirect_relative_url() -> Result<(), Error> { + let username = "user"; + let password = "password"; + + let redirect_server = MockServer::start().await; + + // Configure the redirect server to respond with a 307 with a relative URL. + Mock::given(method("GET")) + .and(path_regex("/foo/bar/baz/")) + .and(basic_auth(username, password)) + .respond_with(ResponseTemplate::new(200)) + .mount(&redirect_server) + .await; + + Mock::given(method("GET")) + .and(path_regex("/foo/")) + .and(basic_auth(username, password)) + .respond_with( + ResponseTemplate::new(307).insert_header("Location", "bar/baz/".to_string()), + ) + .mount(&redirect_server) + .await; + + let cache = Cache::temp()?; + let registry_client = RegistryClientBuilder::new(cache) + .allow_cross_origin_credentials() + .build(); + let client = registry_client.cached_client().uncached(); + + let redirect_server_url = DisplaySafeUrl::parse(&redirect_server.uri())?.join("foo/")?; + let mut url = redirect_server_url.clone(); + let _ = url.set_username(username); + let _ = url.set_password(Some(password)); + + assert_eq!( + client + .for_host(&url) + .get(Url::from(url)) + .send() + .await? + .status(), + 200, + "Requests should succeed for relative URL" + ); + + Ok(()) + } + + #[tokio::test] + async fn test_redirect_preserve_fragment() -> Result<(), Error> { + let redirect_server = MockServer::start().await; + + // Configure the redirect server to respond with a 307 with a relative URL. + Mock::given(method("GET")) + .respond_with(ResponseTemplate::new(307).insert_header("Location", "/foo".to_string())) + .mount(&redirect_server) + .await; + + Mock::given(method("GET")) + .and(path_regex("/foo")) + .respond_with(ResponseTemplate::new(200)) + .mount(&redirect_server) + .await; + + let cache = Cache::temp()?; + let registry_client = RegistryClientBuilder::new(cache).build(); + let client = registry_client.cached_client().uncached(); + + let mut url = DisplaySafeUrl::parse(&redirect_server.uri())?; + url.set_fragment(Some("fragment")); + + assert_eq!( + client + .for_host(&url) + .get(Url::from(url.clone())) + .send() + .await? + .url() + .to_string(), + format!("{}/foo#fragment", redirect_server.uri()), + "Requests should preserve fragment" + ); + + Ok(()) + } + #[test] fn ignore_failing_files() { // 1.7.7 has an invalid requires-python field (double comma), 1.7.8 is valid diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs index 2cf148f2b..90d77bd90 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -1583,7 +1583,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { client .unmanaged .uncached_client(resource.git.repository()) - .clone(), + .raw_client(), ) .await { @@ -1866,7 +1866,10 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { .git() .github_fast_path( git, - client.unmanaged.uncached_client(git.repository()).clone(), + client + .unmanaged + .uncached_client(git.repository()) + .raw_client(), ) .await? .is_some() diff --git a/crates/uv-git/src/resolver.rs b/crates/uv-git/src/resolver.rs index fd90ff587..d404390f3 100644 --- a/crates/uv-git/src/resolver.rs +++ b/crates/uv-git/src/resolver.rs @@ -53,7 +53,7 @@ impl GitResolver { pub async fn github_fast_path( &self, url: &GitUrl, - client: ClientWithMiddleware, + client: &ClientWithMiddleware, ) -> Result, GitResolverError> { if std::env::var_os(EnvVars::UV_NO_GITHUB_FAST_PATH).is_some() { return Ok(None); @@ -117,7 +117,7 @@ impl GitResolver { pub async fn fetch( &self, url: &GitUrl, - client: ClientWithMiddleware, + client: impl Into, disable_ssl: bool, offline: bool, cache: PathBuf, diff --git a/crates/uv-publish/src/lib.rs b/crates/uv-publish/src/lib.rs index dd8358439..ec19713cc 100644 --- a/crates/uv-publish/src/lib.rs +++ b/crates/uv-publish/src/lib.rs @@ -12,7 +12,6 @@ use itertools::Itertools; use reqwest::header::AUTHORIZATION; use reqwest::multipart::Part; use reqwest::{Body, Response, StatusCode}; -use reqwest_middleware::RequestBuilder; use reqwest_retry::policies::ExponentialBackoff; use reqwest_retry::{RetryPolicy, Retryable, RetryableStrategy}; use rustc_hash::FxHashSet; @@ -29,7 +28,7 @@ use uv_auth::Credentials; use uv_cache::{Cache, Refresh}; use uv_client::{ BaseClient, DEFAULT_RETRIES, MetadataFormat, OwnedArchive, RegistryClientBuilder, - UvRetryableStrategy, + RequestBuilder, UvRetryableStrategy, }; use uv_configuration::{KeyringProviderType, TrustedPublishing}; use uv_distribution_filename::{DistFilename, SourceDistExtension, SourceDistFilename}; @@ -330,7 +329,9 @@ pub async fn check_trusted_publishing( debug!( "Running on GitHub Actions without explicit credentials, checking for trusted publishing" ); - match trusted_publishing::get_token(registry, client.for_host(registry)).await { + match trusted_publishing::get_token(registry, client.for_host(registry).raw_client()) + .await + { Ok(token) => Ok(TrustedPublishResult::Configured(token)), Err(err) => { // TODO(konsti): It would be useful if we could differentiate between actual errors @@ -364,7 +365,9 @@ pub async fn check_trusted_publishing( ); } - let token = trusted_publishing::get_token(registry, client.for_host(registry)).await?; + let token = + trusted_publishing::get_token(registry, client.for_host(registry).raw_client()) + .await?; Ok(TrustedPublishResult::Configured(token)) } TrustedPublishing::Never => Ok(TrustedPublishResult::Skipped), @@ -748,16 +751,16 @@ async fn form_metadata( /// Build the upload request. /// /// Returns the request and the reporter progress bar id. -async fn build_request( +async fn build_request<'a>( file: &Path, raw_filename: &str, filename: &DistFilename, registry: &DisplaySafeUrl, - client: &BaseClient, + client: &'a BaseClient, credentials: &Credentials, form_metadata: &[(&'static str, String)], reporter: Arc, -) -> Result<(RequestBuilder, usize), PublishPrepareError> { +) -> Result<(RequestBuilder<'a>, usize), PublishPrepareError> { let mut form = reqwest::multipart::Form::new(); for (key, value) in form_metadata { form = form.text(*key, value.clone()); @@ -969,12 +972,13 @@ mod tests { project_urls: Source, https://github.com/unknown/tqdm "###); + let client = BaseClientBuilder::new().build(); let (request, _) = build_request( &file, raw_filename, &filename, &DisplaySafeUrl::parse("https://example.org/upload").unwrap(), - &BaseClientBuilder::new().build(), + &client, &Credentials::basic(Some("ferris".to_string()), Some("F3RR!S".to_string())), &form_metadata, Arc::new(DummyReporter), @@ -985,7 +989,7 @@ mod tests { insta::with_settings!({ filters => [("boundary=[0-9a-f-]+", "boundary=[...]")], }, { - assert_debug_snapshot!(&request, @r#" + assert_debug_snapshot!(&request.raw_builder(), @r#" RequestBuilder { inner: RequestBuilder { method: POST, @@ -1118,12 +1122,13 @@ mod tests { requires_dist: requests ; extra == 'telegram' "###); + let client = BaseClientBuilder::new().build(); let (request, _) = build_request( &file, raw_filename, &filename, &DisplaySafeUrl::parse("https://example.org/upload").unwrap(), - &BaseClientBuilder::new().build(), + &client, &Credentials::basic(Some("ferris".to_string()), Some("F3RR!S".to_string())), &form_metadata, Arc::new(DummyReporter), @@ -1134,7 +1139,7 @@ mod tests { insta::with_settings!({ filters => [("boundary=[0-9a-f-]+", "boundary=[...]")], }, { - assert_debug_snapshot!(&request, @r#" + assert_debug_snapshot!(&request.raw_builder(), @r#" RequestBuilder { inner: RequestBuilder { method: POST, diff --git a/crates/uv/tests/it/common/mod.rs b/crates/uv/tests/it/common/mod.rs index f997561a9..7ef7cfff6 100644 --- a/crates/uv/tests/it/common/mod.rs +++ b/crates/uv/tests/it/common/mod.rs @@ -1668,9 +1668,9 @@ pub async fn download_to_disk(url: &str, path: &Path) { .allow_insecure_host(trusted_hosts) .build(); let url = url.parse().unwrap(); - let client = client.for_host(&url); let response = client - .request(http::Method::GET, reqwest::Url::from(url)) + .for_host(&url) + .get(reqwest::Url::from(url)) .send() .await .unwrap(); diff --git a/crates/uv/tests/it/edit.rs b/crates/uv/tests/it/edit.rs index 68eae5110..d117b1e8c 100644 --- a/crates/uv/tests/it/edit.rs +++ b/crates/uv/tests/it/edit.rs @@ -14,6 +14,7 @@ use assert_fs::prelude::*; use indoc::{formatdoc, indoc}; use insta::assert_snapshot; use std::path::Path; +use url::Url; use uv_fs::Simplified; use wiremock::{Mock, MockServer, ResponseTemplate, matchers::method}; @@ -11838,6 +11839,196 @@ fn add_auth_policy_never_without_credentials() -> Result<()> { Ok(()) } +/// If uv receives a 302 redirect to a cross-origin server, it should not forward +/// credentials. In the absence of a netrc entry for the new location, +/// it should fail. +#[tokio::test] +async fn add_redirect_cross_origin() -> Result<()> { + let context = TestContext::new("3.12"); + let filters = context + .filters() + .into_iter() + .chain([(r"127\.0\.0\.1:\d*", "[LOCALHOST]")]) + .collect::>(); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! { r#" + [project] + name = "foo" + version = "1.0.0" + requires-python = ">=3.12" + dependencies = [] + "# + })?; + + let redirect_server = MockServer::start().await; + + Mock::given(method("GET")) + .respond_with(|req: &wiremock::Request| { + let redirect_url = redirect_url_to_pypi_proxy(req); + ResponseTemplate::new(302).insert_header("Location", &redirect_url) + }) + .mount(&redirect_server) + .await; + + let mut redirect_url = Url::parse(&redirect_server.uri())?; + let _ = redirect_url.set_username("public"); + let _ = redirect_url.set_password(Some("heron")); + + uv_snapshot!(filters, context.add().arg("--default-index").arg(redirect_url.as_str()).arg("anyio"), @r" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because anyio was not found in the package registry and your project depends on anyio, we can conclude that your project's requirements are unsatisfiable. + + hint: An index URL (http://[LOCALHOST]/) could not be queried due to a lack of valid authentication credentials (401 Unauthorized). + help: If you want to add the package regardless of the failed resolution, provide the `--frozen` flag to skip locking and syncing. + " + ); + + Ok(()) +} + +/// uv currently fails to look up keyring credentials on a cross-origin redirect. +#[tokio::test] +async fn add_redirect_with_keyring_cross_origin() -> Result<()> { + let keyring_context = TestContext::new("3.12"); + + // Install our keyring plugin + keyring_context + .pip_install() + .arg( + keyring_context + .workspace_root + .join("scripts") + .join("packages") + .join("keyring_test_plugin"), + ) + .assert() + .success(); + + let context = TestContext::new("3.12"); + let filters = context + .filters() + .into_iter() + .chain([(r"127\.0\.0\.1:\d*", "[LOCALHOST]")]) + .collect::>(); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! { r#" + [project] + name = "foo" + version = "1.0.0" + requires-python = ">=3.12" + dependencies = [] + + [tool.uv] + keyring-provider = "subprocess" + "#, + })?; + + let redirect_server = MockServer::start().await; + + Mock::given(method("GET")) + .respond_with(|req: &wiremock::Request| { + let redirect_url = redirect_url_to_pypi_proxy(req); + ResponseTemplate::new(302).insert_header("Location", &redirect_url) + }) + .mount(&redirect_server) + .await; + + let mut redirect_url = Url::parse(&redirect_server.uri())?; + let _ = redirect_url.set_username("public"); + + uv_snapshot!(filters, context.add().arg("--default-index") + .arg(redirect_url.as_str()) + .arg("anyio") + .env(EnvVars::KEYRING_TEST_CREDENTIALS, r#"{"pypi-proxy.fly.dev": {"public": "heron"}}"#) + .env(EnvVars::PATH, venv_bin_path(&keyring_context.venv)), @r" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + Keyring request for public@http://[LOCALHOST]/ + Keyring request for public@[LOCALHOST] + × No solution found when resolving dependencies: + ╰─▶ Because anyio was not found in the package registry and your project depends on anyio, we can conclude that your project's requirements are unsatisfiable. + + hint: An index URL (http://[LOCALHOST]/) could not be queried due to a lack of valid authentication credentials (401 Unauthorized). + help: If you want to add the package regardless of the failed resolution, provide the `--frozen` flag to skip locking and syncing. + " + ); + + Ok(()) +} + +/// If uv receives a cross-origin 302 redirect, it should use credentials from netrc +/// for the new location. +#[tokio::test] +async fn pip_install_redirect_with_netrc_cross_origin() -> Result<()> { + let context = TestContext::new("3.12"); + let filters = context + .filters() + .into_iter() + .chain([(r"127\.0\.0\.1:\d*", "[LOCALHOST]")]) + .collect::>(); + + let netrc = context.temp_dir.child(".netrc"); + netrc.write_str("machine pypi-proxy.fly.dev login public password heron")?; + + let redirect_server = MockServer::start().await; + + Mock::given(method("GET")) + .respond_with(|req: &wiremock::Request| { + let redirect_url = redirect_url_to_pypi_proxy(req); + ResponseTemplate::new(302).insert_header("Location", &redirect_url) + }) + .mount(&redirect_server) + .await; + + let mut redirect_url = Url::parse(&redirect_server.uri())?; + let _ = redirect_url.set_username("public"); + + uv_snapshot!(filters, context.pip_install() + .arg("anyio") + .arg("--index-url") + .arg(redirect_url.as_str()) + .env(EnvVars::NETRC, netrc.to_str().unwrap()) + .arg("--strict"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + Prepared 3 packages in [TIME] + Installed 3 packages in [TIME] + + anyio==4.3.0 + + idna==3.6 + + sniffio==1.3.1 + "### + ); + + context.assert_command("import anyio").success(); + + Ok(()) +} + +fn redirect_url_to_pypi_proxy(req: &wiremock::Request) -> String { + let last_path_segment = req + .url + .path_segments() + .expect("path has segments") + .filter(|segment| !segment.is_empty()) // Filter out empty segments + .next_back() + .expect("path has a package segment"); + format!("https://pypi-proxy.fly.dev/basic-auth/simple/{last_path_segment}/") +} + /// Test the error message when adding a package with multiple existing references in /// `pyproject.toml`. #[test] From e9d5780369da14ecf9f834efc8a17b0118823cfb Mon Sep 17 00:00:00 2001 From: John Mumm Date: Fri, 20 Jun 2025 10:17:13 -0400 Subject: [PATCH 052/185] Support transparent Python patch version upgrades (#13954) > NOTE: The PRs that were merged into this feature branch have all been independently reviewed. But it's also useful to see all of the changes in their final form. I've added comments to significant changes throughout the PR to aid discussion. This PR introduces transparent Python version upgrades to uv, allowing for a smoother experience when upgrading to new patch versions. Previously, upgrading Python patch versions required manual updates to each virtual environment. Now, virtual environments can transparently upgrade to newer patch versions. Due to significant changes in how uv installs and executes managed Python executables, this functionality is initially available behind a `--preview` flag. Once an installation has been made upgradeable through `--preview`, subsequent operations (like `uv venv -p 3.10` or patch upgrades) will work without requiring the flag again. This is accomplished by checking for the existence of a minor version symlink directory (or junction on Windows). ### Features * New `uv python upgrade` command to upgrade installed Python versions to the latest available patch release: ``` # Upgrade specific minor version uv python upgrade 3.12 --preview # Upgrade all installed minor versions uv python upgrade --preview ``` * Transparent upgrades also occur when installing newer patch versions: ``` uv python install 3.10.8 --preview # Automatically upgrades existing 3.10 environments uv python install 3.10.18 ``` * Support for transparently upgradeable Python `bin` installations via `--preview` flag ``` uv python install 3.13 --preview # Automatically upgrades the `bin` installation if there is a newer patch version available uv python upgrade 3.13 --preview ``` * Virtual environments can still be tied to a patch version if desired (ignoring patch upgrades): ``` uv venv -p 3.10.8 ``` ### Implementation Transparent upgrades are implemented using: * Minor version symlink directories (Unix) or junctions (Windows) * On Windows, trampolines simulate paths with junctions * Symlink directory naming follows Python build standalone format: e.g., `cpython-3.10-macos-aarch64-none` * Upgrades are scoped to the minor version key (as represented in the naming format: implementation-minor version+variant-os-arch-libc) * If the context does not provide a patch version request and the interpreter is from a managed CPython installation, the `Interpreter` used by `uv python run` will use the full symlink directory executable path when available, enabling transparently upgradeable environments created with the `venv` module (`uv run python -m venv`) New types: * `PythonMinorVersionLink`: in a sense, the core type for this PR, this is a representation of a minor version symlink directory (or junction on Windows) that points to the highest installed managed CPython patch version for a minor version key. * `PythonInstallationMinorVersionKey`: provides a view into a `PythonInstallationKey` that excludes the patch and prerelease. This is used for grouping installations by minor version key (e.g., to find the highest available patch installation for that minor version key) and for minor version directory naming. ### Compatibility * Supports virtual environments created with: * `uv venv` * `uv run python -m venv` (using managed Python that was installed or upgraded with `--preview`) * Virtual environments created within these environments * Existing virtual environments from before these changes continue to work but aren't transparently upgradeable without being recreated * Supports both standard Python (`python3.10`) and freethreaded Python (`python3.10t`) * Support for transparently upgrades is currently only available for managed CPython installations Closes #7287 Closes #7325 Closes #7892 Closes #9031 Closes #12977 --------- Co-authored-by: Zanie Blue --- Cargo.lock | 8 + crates/uv-build-frontend/src/lib.rs | 4 + crates/uv-cli/src/lib.rs | 62 ++ crates/uv-dev/src/compile.rs | 3 +- crates/uv-dispatch/src/lib.rs | 1 + crates/uv-fs/Cargo.toml | 1 - crates/uv-fs/src/path.rs | 15 - crates/uv-python/Cargo.toml | 4 + crates/uv-python/src/discovery.rs | 111 ++- crates/uv-python/src/downloads.rs | 6 +- crates/uv-python/src/environment.rs | 3 + crates/uv-python/src/installation.rs | 147 +++- crates/uv-python/src/interpreter.rs | 52 +- crates/uv-python/src/lib.rs | 94 ++- crates/uv-python/src/managed.rs | 339 +++++++-- crates/uv-python/src/python_version.rs | 2 +- crates/uv-tool/Cargo.toml | 1 + crates/uv-tool/src/lib.rs | 4 + crates/uv-trampoline/src/bounce.rs | 59 +- .../uv-trampoline-aarch64-console.exe | Bin 39424 -> 39936 bytes .../trampolines/uv-trampoline-aarch64-gui.exe | Bin 40448 -> 40960 bytes .../uv-trampoline-i686-console.exe | Bin 34304 -> 34816 bytes .../trampolines/uv-trampoline-i686-gui.exe | Bin 35328 -> 35840 bytes .../uv-trampoline-x86_64-console.exe | Bin 40448 -> 41472 bytes .../trampolines/uv-trampoline-x86_64-gui.exe | Bin 41472 -> 42496 bytes crates/uv-virtualenv/Cargo.toml | 1 + crates/uv-virtualenv/src/lib.rs | 7 + crates/uv-virtualenv/src/virtualenv.rs | 217 +++--- crates/uv/Cargo.toml | 2 + crates/uv/src/commands/build_frontend.rs | 1 + crates/uv/src/commands/pip/check.rs | 3 + crates/uv/src/commands/pip/compile.rs | 16 +- crates/uv/src/commands/pip/freeze.rs | 3 + crates/uv/src/commands/pip/install.rs | 2 + crates/uv/src/commands/pip/list.rs | 4 +- crates/uv/src/commands/pip/show.rs | 3 + crates/uv/src/commands/pip/sync.rs | 2 + crates/uv/src/commands/pip/tree.rs | 4 +- crates/uv/src/commands/pip/uninstall.rs | 4 +- crates/uv/src/commands/project/add.rs | 4 + crates/uv/src/commands/project/environment.rs | 7 +- crates/uv/src/commands/project/export.rs | 2 + crates/uv/src/commands/project/init.rs | 7 + crates/uv/src/commands/project/lock.rs | 3 + crates/uv/src/commands/project/mod.rs | 26 + crates/uv/src/commands/project/remove.rs | 3 + crates/uv/src/commands/project/run.rs | 12 + crates/uv/src/commands/project/sync.rs | 2 + crates/uv/src/commands/project/tree.rs | 2 + crates/uv/src/commands/project/version.rs | 3 + crates/uv/src/commands/python/find.rs | 6 +- crates/uv/src/commands/python/install.rs | 239 ++++-- crates/uv/src/commands/python/list.rs | 3 + crates/uv/src/commands/python/pin.rs | 7 +- crates/uv/src/commands/python/uninstall.rs | 65 +- crates/uv/src/commands/tool/common.rs | 3 + crates/uv/src/commands/tool/install.rs | 4 +- crates/uv/src/commands/tool/run.rs | 2 + crates/uv/src/commands/tool/upgrade.rs | 3 +- crates/uv/src/commands/venv.rs | 14 +- crates/uv/src/lib.rs | 47 +- crates/uv/src/settings.rs | 59 +- crates/uv/tests/it/common/mod.rs | 10 + crates/uv/tests/it/help.rs | 5 + crates/uv/tests/it/main.rs | 3 + crates/uv/tests/it/python_install.rs | 701 ++++++++++++++++- crates/uv/tests/it/python_upgrade.rs | 703 ++++++++++++++++++ crates/uv/tests/it/sync.rs | 8 +- crates/uv/tests/it/tool_upgrade.rs | 12 +- crates/uv/tests/it/venv.rs | 4 +- docs/concepts/python-versions.md | 66 +- docs/guides/install-python.md | 22 + docs/reference/cli.md | 86 +++ 73 files changed, 3022 insertions(+), 306 deletions(-) create mode 100644 crates/uv/tests/it/python_upgrade.rs diff --git a/Cargo.lock b/Cargo.lock index a30a0cbe1..6c1978cd0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4582,6 +4582,7 @@ dependencies = [ "console", "ctrlc", "dotenvy", + "dunce", "etcetera", "filetime", "flate2", @@ -4589,6 +4590,7 @@ dependencies = [ "futures", "http", "ignore", + "indexmap", "indicatif", "indoc", "insta", @@ -5548,15 +5550,18 @@ dependencies = [ "assert_fs", "clap", "configparser", + "dunce", "fs-err 3.1.1", "futures", "goblin", + "indexmap", "indoc", "insta", "itertools 0.14.0", "once_cell", "owo-colors", "procfs", + "ref-cast", "regex", "reqwest", "reqwest-middleware", @@ -5581,6 +5586,7 @@ dependencies = [ "uv-cache-info", "uv-cache-key", "uv-client", + "uv-configuration", "uv-dirs", "uv-distribution-filename", "uv-extract", @@ -5844,6 +5850,7 @@ dependencies = [ "toml_edit", "tracing", "uv-cache", + "uv-configuration", "uv-dirs", "uv-distribution-types", "uv-fs", @@ -5928,6 +5935,7 @@ dependencies = [ "self-replace", "thiserror 2.0.12", "tracing", + "uv-configuration", "uv-fs", "uv-pypi-types", "uv-python", diff --git a/crates/uv-build-frontend/src/lib.rs b/crates/uv-build-frontend/src/lib.rs index 1c29b2c31..34037ffdd 100644 --- a/crates/uv-build-frontend/src/lib.rs +++ b/crates/uv-build-frontend/src/lib.rs @@ -27,6 +27,7 @@ use tokio::process::Command; use tokio::sync::{Mutex, Semaphore}; use tracing::{Instrument, debug, info_span, instrument}; +use uv_configuration::PreviewMode; use uv_configuration::{BuildKind, BuildOutput, ConfigSettings, SourceStrategy}; use uv_distribution::BuildRequires; use uv_distribution_types::{IndexLocations, Requirement, Resolution}; @@ -278,6 +279,7 @@ impl SourceBuild { mut environment_variables: FxHashMap, level: BuildOutput, concurrent_builds: usize, + preview: PreviewMode, ) -> Result { let temp_dir = build_context.cache().venv_dir()?; @@ -325,6 +327,8 @@ impl SourceBuild { false, false, false, + false, + preview, )? }; diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index bd06f9a82..e58f6c079 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -4722,6 +4722,24 @@ pub enum PythonCommand { /// See `uv help python` to view supported request formats. Install(PythonInstallArgs), + /// Upgrade installed Python versions to the latest supported patch release (requires the + /// `--preview` flag). + /// + /// A target Python minor version to upgrade may be provided, e.g., `3.13`. Multiple versions + /// may be provided to perform more than one upgrade. + /// + /// If no target version is provided, then uv will upgrade all managed CPython versions. + /// + /// During an upgrade, uv will not uninstall outdated patch versions. + /// + /// When an upgrade is performed, virtual environments created by uv will automatically + /// use the new version. However, if the virtual environment was created before the + /// upgrade functionality was added, it will continue to use the old Python version; to enable + /// upgrades, the environment must be recreated. + /// + /// Upgrades are not yet supported for alternative implementations, like PyPy. + Upgrade(PythonUpgradeArgs), + /// Search for a Python installation. /// /// Displays the path to the Python executable. @@ -4907,6 +4925,50 @@ pub struct PythonInstallArgs { pub default: bool, } +#[derive(Args)] +pub struct PythonUpgradeArgs { + /// The directory Python installations are stored in. + /// + /// If provided, `UV_PYTHON_INSTALL_DIR` will need to be set for subsequent operations for uv to + /// discover the Python installation. + /// + /// See `uv python dir` to view the current Python installation directory. Defaults to + /// `~/.local/share/uv/python`. + #[arg(long, short, env = EnvVars::UV_PYTHON_INSTALL_DIR)] + pub install_dir: Option, + + /// The Python minor version(s) to upgrade. + /// + /// If no target version is provided, then uv will upgrade all managed CPython versions. + #[arg(env = EnvVars::UV_PYTHON)] + pub targets: Vec, + + /// Set the URL to use as the source for downloading Python installations. + /// + /// The provided URL will replace + /// `https://github.com/astral-sh/python-build-standalone/releases/download` in, e.g., + /// `https://github.com/astral-sh/python-build-standalone/releases/download/20240713/cpython-3.12.4%2B20240713-aarch64-apple-darwin-install_only.tar.gz`. + /// + /// Distributions can be read from a local directory by using the `file://` URL scheme. + #[arg(long, env = EnvVars::UV_PYTHON_INSTALL_MIRROR)] + pub mirror: Option, + + /// Set the URL to use as the source for downloading PyPy installations. + /// + /// The provided URL will replace `https://downloads.python.org/pypy` in, e.g., + /// `https://downloads.python.org/pypy/pypy3.8-v7.3.7-osx64.tar.bz2`. + /// + /// Distributions can be read from a local directory by using the `file://` URL scheme. + #[arg(long, env = EnvVars::UV_PYPY_INSTALL_MIRROR)] + pub pypy_mirror: Option, + + /// URL pointing to JSON of custom Python installations. + /// + /// Note that currently, only local paths are supported. + #[arg(long, env = EnvVars::UV_PYTHON_DOWNLOADS_JSON_URL)] + pub python_downloads_json_url: Option, +} + #[derive(Args)] pub struct PythonUninstallArgs { /// The directory where the Python was installed. diff --git a/crates/uv-dev/src/compile.rs b/crates/uv-dev/src/compile.rs index 434b5e791..d2b685b23 100644 --- a/crates/uv-dev/src/compile.rs +++ b/crates/uv-dev/src/compile.rs @@ -4,7 +4,7 @@ use clap::Parser; use tracing::info; use uv_cache::{Cache, CacheArgs}; -use uv_configuration::Concurrency; +use uv_configuration::{Concurrency, PreviewMode}; use uv_python::{EnvironmentPreference, PythonEnvironment, PythonRequest}; #[derive(Parser)] @@ -26,6 +26,7 @@ pub(crate) async fn compile(args: CompileArgs) -> anyhow::Result<()> { &PythonRequest::default(), EnvironmentPreference::OnlyVirtual, &cache, + PreviewMode::Disabled, )? .into_interpreter(); interpreter.sys_executable().to_path_buf() diff --git a/crates/uv-dispatch/src/lib.rs b/crates/uv-dispatch/src/lib.rs index 3b0ad5555..207f241ad 100644 --- a/crates/uv-dispatch/src/lib.rs +++ b/crates/uv-dispatch/src/lib.rs @@ -433,6 +433,7 @@ impl BuildContext for BuildDispatch<'_> { self.build_extra_env_vars.clone(), build_output, self.concurrency.builds, + self.preview, ) .boxed_local() .await?; diff --git a/crates/uv-fs/Cargo.toml b/crates/uv-fs/Cargo.toml index 12a5f94b7..fba4910e6 100644 --- a/crates/uv-fs/Cargo.toml +++ b/crates/uv-fs/Cargo.toml @@ -16,7 +16,6 @@ doctest = false workspace = true [dependencies] - dunce = { workspace = true } either = { workspace = true } encoding_rs_io = { workspace = true } diff --git a/crates/uv-fs/src/path.rs b/crates/uv-fs/src/path.rs index 7a75c76c3..90d0ce80a 100644 --- a/crates/uv-fs/src/path.rs +++ b/crates/uv-fs/src/path.rs @@ -277,21 +277,6 @@ fn normalized(path: &Path) -> PathBuf { normalized } -/// Like `fs_err::canonicalize`, but avoids attempting to resolve symlinks on Windows. -pub fn canonicalize_executable(path: impl AsRef) -> std::io::Result { - let path = path.as_ref(); - debug_assert!( - path.is_absolute(), - "path must be absolute: {}", - path.display() - ); - if cfg!(windows) { - Ok(path.to_path_buf()) - } else { - fs_err::canonicalize(path) - } -} - /// Compute a path describing `path` relative to `base`. /// /// `lib/python/site-packages/foo/__init__.py` and `lib/python/site-packages` -> `foo/__init__.py` diff --git a/crates/uv-python/Cargo.toml b/crates/uv-python/Cargo.toml index 59a9829e0..d008b2d4e 100644 --- a/crates/uv-python/Cargo.toml +++ b/crates/uv-python/Cargo.toml @@ -20,6 +20,7 @@ uv-cache = { workspace = true } uv-cache-info = { workspace = true } uv-cache-key = { workspace = true } uv-client = { workspace = true } +uv-configuration = { workspace = true } uv-dirs = { workspace = true } uv-distribution-filename = { workspace = true } uv-extract = { workspace = true } @@ -38,11 +39,14 @@ uv-warnings = { workspace = true } anyhow = { workspace = true } clap = { workspace = true, optional = true } configparser = { workspace = true } +dunce = { workspace = true } fs-err = { workspace = true, features = ["tokio"] } futures = { workspace = true } goblin = { workspace = true, default-features = false } +indexmap = { workspace = true } itertools = { workspace = true } owo-colors = { workspace = true } +ref-cast = { workspace = true } regex = { workspace = true } reqwest = { workspace = true } reqwest-middleware = { workspace = true } diff --git a/crates/uv-python/src/discovery.rs b/crates/uv-python/src/discovery.rs index 27853e3db..eaf4b4830 100644 --- a/crates/uv-python/src/discovery.rs +++ b/crates/uv-python/src/discovery.rs @@ -8,6 +8,7 @@ use std::{env, io, iter}; use std::{path::Path, path::PathBuf, str::FromStr}; use thiserror::Error; use tracing::{debug, instrument, trace}; +use uv_configuration::PreviewMode; use which::{which, which_all}; use uv_cache::Cache; @@ -25,7 +26,7 @@ use crate::implementation::ImplementationName; use crate::installation::PythonInstallation; use crate::interpreter::Error as InterpreterError; use crate::interpreter::{StatusCodeError, UnexpectedResponseError}; -use crate::managed::ManagedPythonInstallations; +use crate::managed::{ManagedPythonInstallations, PythonMinorVersionLink}; #[cfg(windows)] use crate::microsoft_store::find_microsoft_store_pythons; use crate::virtualenv::Error as VirtualEnvError; @@ -35,12 +36,12 @@ use crate::virtualenv::{ }; #[cfg(windows)] use crate::windows_registry::{WindowsPython, registry_pythons}; -use crate::{BrokenSymlink, Interpreter, PythonVersion}; +use crate::{BrokenSymlink, Interpreter, PythonInstallationKey, PythonVersion}; /// A request to find a Python installation. /// /// See [`PythonRequest::from_str`]. -#[derive(Debug, Clone, PartialEq, Eq, Default)] +#[derive(Debug, Clone, PartialEq, Eq, Default, Hash)] pub enum PythonRequest { /// An appropriate default Python installation /// @@ -173,7 +174,7 @@ pub enum PythonVariant { } /// A Python discovery version request. -#[derive(Clone, Debug, Default, PartialEq, Eq)] +#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)] pub enum VersionRequest { /// Allow an appropriate default Python version. #[default] @@ -334,6 +335,7 @@ fn python_executables_from_installed<'a>( implementation: Option<&'a ImplementationName>, platform: PlatformRequest, preference: PythonPreference, + preview: PreviewMode, ) -> Box> + 'a> { let from_managed_installations = iter::once_with(move || { ManagedPythonInstallations::from_settings(None) @@ -359,7 +361,29 @@ fn python_executables_from_installed<'a>( true }) .inspect(|installation| debug!("Found managed installation `{installation}`")) - .map(|installation| (PythonSource::Managed, installation.executable(false)))) + .map(move |installation| { + // If it's not a patch version request, then attempt to read the stable + // minor version link. + let executable = version + .patch() + .is_none() + .then(|| { + PythonMinorVersionLink::from_installation( + &installation, + preview, + ) + .filter(PythonMinorVersionLink::exists) + .map( + |minor_version_link| { + minor_version_link.symlink_executable.clone() + }, + ) + }) + .flatten() + .unwrap_or_else(|| installation.executable(false)); + (PythonSource::Managed, executable) + }) + ) }) }) .flatten_ok(); @@ -452,6 +476,7 @@ fn python_executables<'a>( platform: PlatformRequest, environments: EnvironmentPreference, preference: PythonPreference, + preview: PreviewMode, ) -> Box> + 'a> { // Always read from `UV_INTERNAL__PARENT_INTERPRETER` — it could be a system interpreter let from_parent_interpreter = iter::once_with(|| { @@ -472,7 +497,7 @@ fn python_executables<'a>( let from_virtual_environments = python_executables_from_virtual_environments(); let from_installed = - python_executables_from_installed(version, implementation, platform, preference); + python_executables_from_installed(version, implementation, platform, preference, preview); // Limit the search to the relevant environment preference; this avoids unnecessary work like // traversal of the file system. Subsequent filtering should be done by the caller with @@ -671,16 +696,23 @@ fn python_interpreters<'a>( environments: EnvironmentPreference, preference: PythonPreference, cache: &'a Cache, + preview: PreviewMode, ) -> impl Iterator> + 'a { python_interpreters_from_executables( // Perform filtering on the discovered executables based on their source. This avoids // unnecessary interpreter queries, which are generally expensive. We'll filter again // with `interpreter_satisfies_environment_preference` after querying. - python_executables(version, implementation, platform, environments, preference).filter_ok( - move |(source, path)| { - source_satisfies_environment_preference(*source, path, environments) - }, - ), + python_executables( + version, + implementation, + platform, + environments, + preference, + preview, + ) + .filter_ok(move |(source, path)| { + source_satisfies_environment_preference(*source, path, environments) + }), cache, ) .filter_ok(move |(source, interpreter)| { @@ -919,6 +951,7 @@ pub fn find_python_installations<'a>( environments: EnvironmentPreference, preference: PythonPreference, cache: &'a Cache, + preview: PreviewMode, ) -> Box> + 'a> { let sources = DiscoveryPreferences { python_preference: preference, @@ -1010,6 +1043,7 @@ pub fn find_python_installations<'a>( environments, preference, cache, + preview, ) .map_ok(|tuple| Ok(PythonInstallation::from_tuple(tuple))) }), @@ -1022,6 +1056,7 @@ pub fn find_python_installations<'a>( environments, preference, cache, + preview, ) .map_ok(|tuple| Ok(PythonInstallation::from_tuple(tuple))) }), @@ -1038,6 +1073,7 @@ pub fn find_python_installations<'a>( environments, preference, cache, + preview, ) .map_ok(|tuple| Ok(PythonInstallation::from_tuple(tuple))) }) @@ -1051,6 +1087,7 @@ pub fn find_python_installations<'a>( environments, preference, cache, + preview, ) .filter_ok(|(_source, interpreter)| { interpreter @@ -1072,6 +1109,7 @@ pub fn find_python_installations<'a>( environments, preference, cache, + preview, ) .filter_ok(|(_source, interpreter)| { interpreter @@ -1096,6 +1134,7 @@ pub fn find_python_installations<'a>( environments, preference, cache, + preview, ) .filter_ok(|(_source, interpreter)| request.satisfied_by_interpreter(interpreter)) .map_ok(|tuple| Ok(PythonInstallation::from_tuple(tuple))) @@ -1113,8 +1152,10 @@ pub(crate) fn find_python_installation( environments: EnvironmentPreference, preference: PythonPreference, cache: &Cache, + preview: PreviewMode, ) -> Result { - let installations = find_python_installations(request, environments, preference, cache); + let installations = + find_python_installations(request, environments, preference, cache, preview); let mut first_prerelease = None; let mut first_error = None; for result in installations { @@ -1210,12 +1251,13 @@ pub(crate) fn find_best_python_installation( environments: EnvironmentPreference, preference: PythonPreference, cache: &Cache, + preview: PreviewMode, ) -> Result { debug!("Starting Python discovery for {}", request); // First, check for an exact match (or the first available version if no Python version was provided) debug!("Looking for exact match for request {request}"); - let result = find_python_installation(request, environments, preference, cache); + let result = find_python_installation(request, environments, preference, cache, preview); match result { Ok(Ok(installation)) => { warn_on_unsupported_python(installation.interpreter()); @@ -1243,7 +1285,7 @@ pub(crate) fn find_best_python_installation( _ => None, } { debug!("Looking for relaxed patch version {request}"); - let result = find_python_installation(&request, environments, preference, cache); + let result = find_python_installation(&request, environments, preference, cache, preview); match result { Ok(Ok(installation)) => { warn_on_unsupported_python(installation.interpreter()); @@ -1260,14 +1302,16 @@ pub(crate) fn find_best_python_installation( debug!("Looking for a default Python installation"); let request = PythonRequest::Default; Ok( - find_python_installation(&request, environments, preference, cache)?.map_err(|err| { - // Use a more general error in this case since we looked for multiple versions - PythonNotFound { - request, - python_preference: err.python_preference, - environment_preference: err.environment_preference, - } - }), + find_python_installation(&request, environments, preference, cache, preview)?.map_err( + |err| { + // Use a more general error in this case since we looked for multiple versions + PythonNotFound { + request, + python_preference: err.python_preference, + environment_preference: err.environment_preference, + } + }, + ), ) } @@ -1645,6 +1689,24 @@ impl PythonRequest { Ok(rest.parse().ok()) } + /// Check if this request includes a specific patch version. + pub fn includes_patch(&self) -> bool { + match self { + PythonRequest::Default => false, + PythonRequest::Any => false, + PythonRequest::Version(version_request) => version_request.patch().is_some(), + PythonRequest::Directory(..) => false, + PythonRequest::File(..) => false, + PythonRequest::ExecutableName(..) => false, + PythonRequest::Implementation(..) => false, + PythonRequest::ImplementationVersion(_, version) => version.patch().is_some(), + PythonRequest::Key(request) => request + .version + .as_ref() + .is_some_and(|request| request.patch().is_some()), + } + } + /// Check if a given interpreter satisfies the interpreter request. pub fn satisfied(&self, interpreter: &Interpreter, cache: &Cache) -> bool { /// Returns `true` if the two paths refer to the same interpreter executable. @@ -2086,6 +2148,11 @@ impl fmt::Display for ExecutableName { } impl VersionRequest { + /// Derive a [`VersionRequest::MajorMinor`] from a [`PythonInstallationKey`] + pub fn major_minor_request_from_key(key: &PythonInstallationKey) -> Self { + Self::MajorMinor(key.major, key.minor, key.variant) + } + /// Return possible executable names for the given version request. pub(crate) fn executable_names( &self, diff --git a/crates/uv-python/src/downloads.rs b/crates/uv-python/src/downloads.rs index fd3f6c417..51f6f1d45 100644 --- a/crates/uv-python/src/downloads.rs +++ b/crates/uv-python/src/downloads.rs @@ -111,14 +111,14 @@ pub enum Error { }, } -#[derive(Debug, PartialEq, Clone)] +#[derive(Debug, PartialEq, Eq, Clone, Hash)] pub struct ManagedPythonDownload { key: PythonInstallationKey, url: &'static str, sha256: Option<&'static str>, } -#[derive(Debug, Clone, Default, Eq, PartialEq)] +#[derive(Debug, Clone, Default, Eq, PartialEq, Hash)] pub struct PythonDownloadRequest { pub(crate) version: Option, pub(crate) implementation: Option, @@ -131,7 +131,7 @@ pub struct PythonDownloadRequest { pub(crate) prereleases: Option, } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum ArchRequest { Explicit(Arch), Environment(Arch), diff --git a/crates/uv-python/src/environment.rs b/crates/uv-python/src/environment.rs index 34fa3eee9..02f9fd683 100644 --- a/crates/uv-python/src/environment.rs +++ b/crates/uv-python/src/environment.rs @@ -7,6 +7,7 @@ use owo_colors::OwoColorize; use tracing::debug; use uv_cache::Cache; +use uv_configuration::PreviewMode; use uv_fs::{LockedFile, Simplified}; use uv_pep440::Version; @@ -152,6 +153,7 @@ impl PythonEnvironment { request: &PythonRequest, preference: EnvironmentPreference, cache: &Cache, + preview: PreviewMode, ) -> Result { let installation = match find_python_installation( request, @@ -159,6 +161,7 @@ impl PythonEnvironment { // Ignore managed installations when looking for environments PythonPreference::OnlySystem, cache, + preview, )? { Ok(installation) => installation, Err(err) => return Err(EnvironmentNotFound::from(err).into()), diff --git a/crates/uv-python/src/installation.rs b/crates/uv-python/src/installation.rs index 611dc2007..d46643d21 100644 --- a/crates/uv-python/src/installation.rs +++ b/crates/uv-python/src/installation.rs @@ -1,10 +1,14 @@ use std::fmt; +use std::hash::{Hash, Hasher}; use std::str::FromStr; +use indexmap::IndexMap; +use ref_cast::RefCast; use tracing::{debug, info}; use uv_cache::Cache; use uv_client::BaseClientBuilder; +use uv_configuration::PreviewMode; use uv_pep440::{Prerelease, Version}; use crate::discovery::{ @@ -54,8 +58,10 @@ impl PythonInstallation { environments: EnvironmentPreference, preference: PythonPreference, cache: &Cache, + preview: PreviewMode, ) -> Result { - let installation = find_python_installation(request, environments, preference, cache)??; + let installation = + find_python_installation(request, environments, preference, cache, preview)??; Ok(installation) } @@ -66,12 +72,14 @@ impl PythonInstallation { environments: EnvironmentPreference, preference: PythonPreference, cache: &Cache, + preview: PreviewMode, ) -> Result { Ok(find_best_python_installation( request, environments, preference, cache, + preview, )??) } @@ -89,11 +97,12 @@ impl PythonInstallation { python_install_mirror: Option<&str>, pypy_install_mirror: Option<&str>, python_downloads_json_url: Option<&str>, + preview: PreviewMode, ) -> Result { let request = request.unwrap_or(&PythonRequest::Default); // Search for the installation - let err = match Self::find(request, environments, preference, cache) { + let err = match Self::find(request, environments, preference, cache, preview) { Ok(installation) => return Ok(installation), Err(err) => err, }; @@ -129,6 +138,7 @@ impl PythonInstallation { python_install_mirror, pypy_install_mirror, python_downloads_json_url, + preview, ) .await { @@ -149,6 +159,7 @@ impl PythonInstallation { python_install_mirror: Option<&str>, pypy_install_mirror: Option<&str>, python_downloads_json_url: Option<&str>, + preview: PreviewMode, ) -> Result { let installations = ManagedPythonInstallations::from_settings(None)?.init()?; let installations_dir = installations.root(); @@ -180,6 +191,21 @@ impl PythonInstallation { installed.ensure_externally_managed()?; installed.ensure_sysconfig_patched()?; installed.ensure_canonical_executables()?; + + let minor_version = installed.minor_version_key(); + let highest_patch = installations + .find_all()? + .filter(|installation| installation.minor_version_key() == minor_version) + .filter_map(|installation| installation.version().patch()) + .fold(0, std::cmp::max); + if installed + .version() + .patch() + .is_some_and(|p| p >= highest_patch) + { + installed.ensure_minor_version_link(preview)?; + } + if let Err(e) = installed.ensure_dylib_patched() { e.warn_user(&installed); } @@ -340,6 +366,14 @@ impl PythonInstallationKey { format!("{}.{}.{}", self.major, self.minor, self.patch) } + pub fn major(&self) -> u8 { + self.major + } + + pub fn minor(&self) -> u8 { + self.minor + } + pub fn arch(&self) -> &Arch { &self.arch } @@ -490,3 +524,112 @@ impl Ord for PythonInstallationKey { .then_with(|| self.variant.cmp(&other.variant).reverse()) } } + +/// A view into a [`PythonInstallationKey`] that excludes the patch and prerelease versions. +#[derive(Clone, Eq, Ord, PartialOrd, RefCast)] +#[repr(transparent)] +pub struct PythonInstallationMinorVersionKey(PythonInstallationKey); + +impl PythonInstallationMinorVersionKey { + /// Cast a `&PythonInstallationKey` to a `&PythonInstallationMinorVersionKey` using ref-cast. + #[inline] + pub fn ref_cast(key: &PythonInstallationKey) -> &Self { + RefCast::ref_cast(key) + } + + /// Takes an [`IntoIterator`] of [`ManagedPythonInstallation`]s and returns an [`FxHashMap`] from + /// [`PythonInstallationMinorVersionKey`] to the installation with highest [`PythonInstallationKey`] + /// for that minor version key. + #[inline] + pub fn highest_installations_by_minor_version_key<'a, I>( + installations: I, + ) -> IndexMap + where + I: IntoIterator, + { + let mut minor_versions = IndexMap::default(); + for installation in installations { + minor_versions + .entry(installation.minor_version_key().clone()) + .and_modify(|high_installation: &mut ManagedPythonInstallation| { + if installation.key() >= high_installation.key() { + *high_installation = installation.clone(); + } + }) + .or_insert_with(|| installation.clone()); + } + minor_versions + } +} + +impl fmt::Display for PythonInstallationMinorVersionKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // Display every field on the wrapped key except the patch + // and prerelease (with special formatting for the variant). + let variant = match self.0.variant { + PythonVariant::Default => String::new(), + PythonVariant::Freethreaded => format!("+{}", self.0.variant), + }; + write!( + f, + "{}-{}.{}{}-{}-{}-{}", + self.0.implementation, + self.0.major, + self.0.minor, + variant, + self.0.os, + self.0.arch, + self.0.libc, + ) + } +} + +impl fmt::Debug for PythonInstallationMinorVersionKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // Display every field on the wrapped key except the patch + // and prerelease. + f.debug_struct("PythonInstallationMinorVersionKey") + .field("implementation", &self.0.implementation) + .field("major", &self.0.major) + .field("minor", &self.0.minor) + .field("variant", &self.0.variant) + .field("os", &self.0.os) + .field("arch", &self.0.arch) + .field("libc", &self.0.libc) + .finish() + } +} + +impl PartialEq for PythonInstallationMinorVersionKey { + fn eq(&self, other: &Self) -> bool { + // Compare every field on the wrapped key except the patch + // and prerelease. + self.0.implementation == other.0.implementation + && self.0.major == other.0.major + && self.0.minor == other.0.minor + && self.0.os == other.0.os + && self.0.arch == other.0.arch + && self.0.libc == other.0.libc + && self.0.variant == other.0.variant + } +} + +impl Hash for PythonInstallationMinorVersionKey { + fn hash(&self, state: &mut H) { + // Hash every field on the wrapped key except the patch + // and prerelease. + self.0.implementation.hash(state); + self.0.major.hash(state); + self.0.minor.hash(state); + self.0.os.hash(state); + self.0.arch.hash(state); + self.0.libc.hash(state); + self.0.variant.hash(state); + } +} + +impl From for PythonInstallationMinorVersionKey { + fn from(key: PythonInstallationKey) -> Self { + PythonInstallationMinorVersionKey(key) + } +} diff --git a/crates/uv-python/src/interpreter.rs b/crates/uv-python/src/interpreter.rs index 26d810db5..19e790b7e 100644 --- a/crates/uv-python/src/interpreter.rs +++ b/crates/uv-python/src/interpreter.rs @@ -26,6 +26,7 @@ use uv_platform_tags::{Tags, TagsError}; use uv_pypi_types::{ResolverMarkerEnvironment, Scheme}; use crate::implementation::LenientImplementationName; +use crate::managed::ManagedPythonInstallations; use crate::platform::{Arch, Libc, Os}; use crate::pointer_size::PointerSize; use crate::{ @@ -168,7 +169,7 @@ impl Interpreter { Ok(path) => path, Err(err) => { warn!("Failed to find base Python executable: {err}"); - uv_fs::canonicalize_executable(base_executable)? + canonicalize_executable(base_executable)? } }; Ok(base_python) @@ -263,6 +264,21 @@ impl Interpreter { self.prefix.is_some() } + /// Returns `true` if this interpreter is managed by uv. + /// + /// Returns `false` if we cannot determine the path of the uv managed Python interpreters. + pub fn is_managed(&self) -> bool { + let Ok(installations) = ManagedPythonInstallations::from_settings(None) else { + return false; + }; + + installations + .find_all() + .into_iter() + .flatten() + .any(|install| install.path() == self.sys_base_prefix) + } + /// Returns `Some` if the environment is externally managed, optionally including an error /// message from the `EXTERNALLY-MANAGED` file. /// @@ -483,10 +499,19 @@ impl Interpreter { /// `python-build-standalone`. /// /// See: + #[cfg(unix)] pub fn is_standalone(&self) -> bool { self.standalone } + /// Returns `true` if an [`Interpreter`] may be a `python-build-standalone` interpreter. + // TODO(john): Replace this approach with patching sysconfig on Windows to + // set `PYTHON_BUILD_STANDALONE=1`.` + #[cfg(windows)] + pub fn is_standalone(&self) -> bool { + self.standalone || (self.is_managed() && self.markers().implementation_name() == "cpython") + } + /// Return the [`Layout`] environment used to install wheels into this interpreter. pub fn layout(&self) -> Layout { Layout { @@ -608,6 +633,29 @@ impl Interpreter { } } +/// Calls `fs_err::canonicalize` on Unix. On Windows, avoids attempting to resolve symlinks +/// but will resolve junctions if they are part of a trampoline target. +pub fn canonicalize_executable(path: impl AsRef) -> std::io::Result { + let path = path.as_ref(); + debug_assert!( + path.is_absolute(), + "path must be absolute: {}", + path.display() + ); + + #[cfg(windows)] + { + if let Ok(Some(launcher)) = uv_trampoline_builder::Launcher::try_from_path(path) { + Ok(dunce::canonicalize(launcher.python_path)?) + } else { + Ok(path.to_path_buf()) + } + } + + #[cfg(unix)] + fs_err::canonicalize(path) +} + /// The `EXTERNALLY-MANAGED` file in a Python installation. /// /// See: @@ -935,7 +983,7 @@ impl InterpreterInfo { // We check the timestamp of the canonicalized executable to check if an underlying // interpreter has been modified. - let modified = uv_fs::canonicalize_executable(&absolute) + let modified = canonicalize_executable(&absolute) .and_then(Timestamp::from_path) .map_err(|err| { if err.kind() == io::ErrorKind::NotFound { diff --git a/crates/uv-python/src/lib.rs b/crates/uv-python/src/lib.rs index 0fa2d46ab..d408bc199 100644 --- a/crates/uv-python/src/lib.rs +++ b/crates/uv-python/src/lib.rs @@ -11,9 +11,13 @@ pub use crate::discovery::{ }; pub use crate::downloads::PlatformRequest; pub use crate::environment::{InvalidEnvironmentKind, PythonEnvironment}; -pub use crate::implementation::ImplementationName; -pub use crate::installation::{PythonInstallation, PythonInstallationKey}; -pub use crate::interpreter::{BrokenSymlink, Error as InterpreterError, Interpreter}; +pub use crate::implementation::{ImplementationName, LenientImplementationName}; +pub use crate::installation::{ + PythonInstallation, PythonInstallationKey, PythonInstallationMinorVersionKey, +}; +pub use crate::interpreter::{ + BrokenSymlink, Error as InterpreterError, Interpreter, canonicalize_executable, +}; pub use crate::pointer_size::PointerSize; pub use crate::prefix::Prefix; pub use crate::python_version::PythonVersion; @@ -115,6 +119,7 @@ mod tests { use indoc::{formatdoc, indoc}; use temp_env::with_vars; use test_log::test; + use uv_configuration::PreviewMode; use uv_static::EnvVars; use uv_cache::Cache; @@ -447,6 +452,7 @@ mod tests { EnvironmentPreference::OnlySystem, PythonPreference::default(), &context.cache, + PreviewMode::Disabled, ) }); assert!( @@ -461,6 +467,7 @@ mod tests { EnvironmentPreference::OnlySystem, PythonPreference::default(), &context.cache, + PreviewMode::Disabled, ) }); assert!( @@ -485,6 +492,7 @@ mod tests { EnvironmentPreference::OnlySystem, PythonPreference::default(), &context.cache, + PreviewMode::Disabled, ) }); assert!( @@ -506,6 +514,7 @@ mod tests { EnvironmentPreference::OnlySystem, PythonPreference::default(), &context.cache, + PreviewMode::Disabled, ) })??; assert!( @@ -567,6 +576,7 @@ mod tests { EnvironmentPreference::OnlySystem, PythonPreference::default(), &context.cache, + PreviewMode::Disabled, ) })??; assert!( @@ -598,6 +608,7 @@ mod tests { EnvironmentPreference::OnlySystem, PythonPreference::default(), &context.cache, + PreviewMode::Disabled, ) }); assert!( @@ -634,6 +645,7 @@ mod tests { EnvironmentPreference::OnlySystem, PythonPreference::default(), &context.cache, + PreviewMode::Disabled, ) })??; assert!( @@ -665,6 +677,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) })??; assert_eq!( @@ -686,6 +699,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) })??; assert_eq!( @@ -711,6 +725,7 @@ mod tests { EnvironmentPreference::OnlySystem, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) })??; assert_eq!( @@ -736,6 +751,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) })??; assert_eq!( @@ -758,6 +774,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) })??; @@ -791,6 +808,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) })??; @@ -824,6 +842,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) })?; assert!( @@ -845,6 +864,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) })?; assert!( @@ -866,6 +886,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) })??; @@ -899,6 +920,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) })??; @@ -935,6 +957,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) })??; assert!( @@ -965,6 +988,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) })??; assert!( @@ -999,6 +1023,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) })??; assert_eq!( @@ -1024,6 +1049,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) })??; assert_eq!( @@ -1050,6 +1076,7 @@ mod tests { EnvironmentPreference::OnlyVirtual, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) }, )??; @@ -1074,6 +1101,7 @@ mod tests { EnvironmentPreference::OnlyVirtual, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) }, )?; @@ -1095,6 +1123,7 @@ mod tests { EnvironmentPreference::OnlySystem, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) }, )??; @@ -1117,6 +1146,7 @@ mod tests { EnvironmentPreference::OnlyVirtual, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) }, )??; @@ -1149,6 +1179,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) }, )??; @@ -1169,6 +1200,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) }, )??; @@ -1195,6 +1227,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) })??; @@ -1212,6 +1245,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) })??; @@ -1240,6 +1274,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) })??; assert_eq!( @@ -1277,6 +1312,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) }, )??; @@ -1304,6 +1340,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) }, )??; @@ -1328,6 +1365,7 @@ mod tests { EnvironmentPreference::ExplicitSystem, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) }, )??; @@ -1352,6 +1390,7 @@ mod tests { EnvironmentPreference::OnlySystem, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) }, )??; @@ -1376,6 +1415,7 @@ mod tests { EnvironmentPreference::OnlyVirtual, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) }, )??; @@ -1413,6 +1453,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) }, )??; @@ -1440,6 +1481,7 @@ mod tests { EnvironmentPreference::OnlySystem, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) })??; assert_eq!( @@ -1456,6 +1498,7 @@ mod tests { EnvironmentPreference::OnlySystem, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) })??; assert_eq!( @@ -1472,6 +1515,7 @@ mod tests { EnvironmentPreference::OnlySystem, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) })?; assert!( @@ -1493,6 +1537,7 @@ mod tests { EnvironmentPreference::OnlyVirtual, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) })?; assert!( @@ -1509,6 +1554,7 @@ mod tests { EnvironmentPreference::OnlySystem, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) }, )?; @@ -1530,6 +1576,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) })??; assert_eq!( @@ -1544,6 +1591,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) })?; assert!( @@ -1557,6 +1605,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) })?; assert!( @@ -1585,6 +1634,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) })??; assert_eq!( @@ -1600,6 +1650,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) })??; assert_eq!( @@ -1629,6 +1680,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) })??; assert_eq!( @@ -1644,6 +1696,7 @@ mod tests { EnvironmentPreference::ExplicitSystem, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) })??; assert_eq!( @@ -1659,6 +1712,7 @@ mod tests { EnvironmentPreference::OnlyVirtual, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) })??; assert_eq!( @@ -1674,6 +1728,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) })??; assert_eq!( @@ -1697,6 +1752,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) })??; assert_eq!( @@ -1711,6 +1767,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) })??; assert_eq!( @@ -1734,6 +1791,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) })??; assert_eq!( @@ -1753,6 +1811,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) }, )??; @@ -1781,6 +1840,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) })??; assert_eq!( @@ -1802,6 +1862,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) })?; assert!( @@ -1831,6 +1892,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) })??; assert_eq!( @@ -1846,6 +1908,7 @@ mod tests { EnvironmentPreference::ExplicitSystem, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) })?; assert!( @@ -1872,6 +1935,7 @@ mod tests { EnvironmentPreference::ExplicitSystem, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) }) .unwrap() @@ -1896,6 +1960,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) })?; assert!( @@ -1912,6 +1977,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) })??; assert_eq!( @@ -1926,6 +1992,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) })??; assert_eq!( @@ -1951,6 +2018,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) })??; assert_eq!( @@ -1965,6 +2033,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) })??; assert_eq!( @@ -1990,6 +2059,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) })??; assert_eq!( @@ -2016,6 +2086,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) })??; assert_eq!( @@ -2042,6 +2113,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) })??; assert_eq!( @@ -2068,6 +2140,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) })??; assert_eq!( @@ -2094,6 +2167,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) })??; assert_eq!( @@ -2121,6 +2195,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) })?; assert!( @@ -2142,6 +2217,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) })??; assert_eq!( @@ -2156,6 +2232,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) })??; assert_eq!( @@ -2181,6 +2258,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) })??; assert_eq!( @@ -2195,6 +2273,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) })??; assert_eq!( @@ -2232,6 +2311,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) }) .unwrap() @@ -2249,6 +2329,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) }) .unwrap() @@ -2290,6 +2371,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) }) .unwrap() @@ -2307,6 +2389,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) }) .unwrap() @@ -2343,6 +2426,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) }) .unwrap() @@ -2365,6 +2449,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) }) .unwrap() @@ -2387,6 +2472,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) }) .unwrap() @@ -2425,6 +2511,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) })??; @@ -2477,6 +2564,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, + PreviewMode::Disabled, ) })??; diff --git a/crates/uv-python/src/managed.rs b/crates/uv-python/src/managed.rs index edf4087e7..e7287fe72 100644 --- a/crates/uv-python/src/managed.rs +++ b/crates/uv-python/src/managed.rs @@ -2,6 +2,8 @@ use core::fmt; use std::cmp::Reverse; use std::ffi::OsStr; use std::io::{self, Write}; +#[cfg(windows)] +use std::os::windows::fs::MetadataExt; use std::path::{Path, PathBuf}; use std::str::FromStr; @@ -10,8 +12,11 @@ use itertools::Itertools; use same_file::is_same_file; use thiserror::Error; use tracing::{debug, warn}; +use uv_configuration::PreviewMode; +#[cfg(windows)] +use windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_REPARSE_POINT; -use uv_fs::{LockedFile, Simplified, symlink_or_copy_file}; +use uv_fs::{LockedFile, Simplified, replace_symlink, symlink_or_copy_file}; use uv_state::{StateBucket, StateStore}; use uv_static::EnvVars; use uv_trampoline_builder::{Launcher, windows_python_launcher}; @@ -25,7 +30,9 @@ use crate::libc::LibcDetectionError; use crate::platform::Error as PlatformError; use crate::platform::{Arch, Libc, Os}; use crate::python_version::PythonVersion; -use crate::{PythonRequest, PythonVariant, macos_dylib, sysconfig}; +use crate::{ + PythonInstallationMinorVersionKey, PythonRequest, PythonVariant, macos_dylib, sysconfig, +}; #[derive(Error, Debug)] pub enum Error { @@ -51,6 +58,8 @@ pub enum Error { }, #[error("Missing expected Python executable at {}", _0.user_display())] MissingExecutable(PathBuf), + #[error("Missing expected target directory for Python minor version link at {}", _0.user_display())] + MissingPythonMinorVersionLinkTargetDirectory(PathBuf), #[error("Failed to create canonical Python executable at {} from {}", to.user_display(), from.user_display())] CanonicalizeExecutable { from: PathBuf, @@ -65,6 +74,13 @@ pub enum Error { #[source] err: io::Error, }, + #[error("Failed to create Python minor version link directory at {} from {}", to.user_display(), from.user_display())] + PythonMinorVersionLinkDirectory { + from: PathBuf, + to: PathBuf, + #[source] + err: io::Error, + }, #[error("Failed to create directory for Python executable link at {}", to.user_display())] ExecutableDirectory { to: PathBuf, @@ -339,7 +355,7 @@ impl ManagedPythonInstallation { /// The path to this managed installation's Python executable. /// - /// If the installation has multiple execututables i.e., `python`, `python3`, etc., this will + /// If the installation has multiple executables i.e., `python`, `python3`, etc., this will /// return the _canonical_ executable name which the other names link to. On Unix, this is /// `python{major}.{minor}{variant}` and on Windows, this is `python{exe}`. /// @@ -383,13 +399,11 @@ impl ManagedPythonInstallation { exe = std::env::consts::EXE_SUFFIX ); - let executable = if cfg!(unix) || *self.implementation() == ImplementationName::GraalPy { - self.python_dir().join("bin").join(name) - } else if cfg!(windows) { - self.python_dir().join(name) - } else { - unimplemented!("Only Windows and Unix systems are supported.") - }; + let executable = executable_path_from_base( + self.python_dir().as_path(), + &name, + &LenientImplementationName::from(*self.implementation()), + ); // Workaround for python-build-standalone v20241016 which is missing the standard // `python.exe` executable in free-threaded distributions on Windows. @@ -442,6 +456,10 @@ impl ManagedPythonInstallation { &self.key } + pub fn minor_version_key(&self) -> &PythonInstallationMinorVersionKey { + PythonInstallationMinorVersionKey::ref_cast(&self.key) + } + pub fn satisfies(&self, request: &PythonRequest) -> bool { match request { PythonRequest::File(path) => self.executable(false) == *path, @@ -503,6 +521,30 @@ impl ManagedPythonInstallation { Ok(()) } + /// Ensure the environment contains the symlink directory (or junction on Windows) + /// pointing to the patch directory for this minor version. + pub fn ensure_minor_version_link(&self, preview: PreviewMode) -> Result<(), Error> { + if let Some(minor_version_link) = PythonMinorVersionLink::from_installation(self, preview) { + minor_version_link.create_directory()?; + } + Ok(()) + } + + /// If the environment contains a symlink directory (or junction on Windows), + /// update it to the latest patch directory for this minor version. + /// + /// Unlike [`ensure_minor_version_link`], will not create a new symlink directory + /// if one doesn't already exist, + pub fn update_minor_version_link(&self, preview: PreviewMode) -> Result<(), Error> { + if let Some(minor_version_link) = PythonMinorVersionLink::from_installation(self, preview) { + if !minor_version_link.exists() { + return Ok(()); + } + minor_version_link.create_directory()?; + } + Ok(()) + } + /// Ensure the environment is marked as externally managed with the /// standard `EXTERNALLY-MANAGED` file. pub fn ensure_externally_managed(&self) -> Result<(), Error> { @@ -567,54 +609,8 @@ impl ManagedPythonInstallation { Ok(()) } - /// Create a link to the managed Python executable. - /// - /// If the file already exists at the target path, an error will be returned. - pub fn create_bin_link(&self, target: &Path) -> Result<(), Error> { - let python = self.executable(false); - - let bin = target.parent().ok_or(Error::NoExecutableDirectory)?; - fs_err::create_dir_all(bin).map_err(|err| Error::ExecutableDirectory { - to: bin.to_path_buf(), - err, - })?; - - if cfg!(unix) { - // Note this will never copy on Unix — we use it here to allow compilation on Windows - match symlink_or_copy_file(&python, target) { - Ok(()) => Ok(()), - Err(err) if err.kind() == io::ErrorKind::NotFound => { - Err(Error::MissingExecutable(python.clone())) - } - Err(err) => Err(Error::LinkExecutable { - from: python, - to: target.to_path_buf(), - err, - }), - } - } else if cfg!(windows) { - // TODO(zanieb): Install GUI launchers as well - let launcher = windows_python_launcher(&python, false)?; - - // OK to use `std::fs` here, `fs_err` does not support `File::create_new` and we attach - // error context anyway - #[allow(clippy::disallowed_types)] - { - std::fs::File::create_new(target) - .and_then(|mut file| file.write_all(launcher.as_ref())) - .map_err(|err| Error::LinkExecutable { - from: python, - to: target.to_path_buf(), - err, - }) - } - } else { - unimplemented!("Only Windows and Unix systems are supported.") - } - } - /// Returns `true` if the path is a link to this installation's binary, e.g., as created by - /// [`ManagedPythonInstallation::create_bin_link`]. + /// [`create_bin_link`]. pub fn is_bin_link(&self, path: &Path) -> bool { if cfg!(unix) { is_same_file(path, self.executable(false)).unwrap_or_default() @@ -625,7 +621,11 @@ impl ManagedPythonInstallation { if !matches!(launcher.kind, uv_trampoline_builder::LauncherKind::Python) { return false; } - launcher.python_path == self.executable(false) + // We canonicalize the target path of the launcher in case it includes a minor version + // junction directory. If canonicalization fails, we check against the launcher path + // directly. + dunce::canonicalize(&launcher.python_path).unwrap_or(launcher.python_path) + == self.executable(false) } else { unreachable!("Only Windows and Unix are supported") } @@ -669,6 +669,229 @@ impl ManagedPythonInstallation { } } +/// A representation of a minor version symlink directory (or junction on Windows) +/// linking to the home directory of a Python installation. +#[derive(Clone, Debug)] +pub struct PythonMinorVersionLink { + /// The symlink directory (or junction on Windows). + pub symlink_directory: PathBuf, + /// The full path to the executable including the symlink directory + /// (or junction on Windows). + pub symlink_executable: PathBuf, + /// The target directory for the symlink. This is the home directory for + /// a Python installation. + pub target_directory: PathBuf, +} + +impl PythonMinorVersionLink { + /// Attempt to derive a path from an executable path that substitutes a minor + /// version symlink directory (or junction on Windows) for the patch version + /// directory. + /// + /// The implementation is expected to be CPython and, on Unix, the base Python is + /// expected to be in `/bin/` on Unix. If either condition isn't true, + /// return [`None`]. + /// + /// # Examples + /// + /// ## Unix + /// For a Python 3.10.8 installation in `/path/to/uv/python/cpython-3.10.8-macos-aarch64-none/bin/python3.10`, + /// the symlink directory would be `/path/to/uv/python/cpython-3.10-macos-aarch64-none` and the executable path including the + /// symlink directory would be `/path/to/uv/python/cpython-3.10-macos-aarch64-none/bin/python3.10`. + /// + /// ## Windows + /// For a Python 3.10.8 installation in `C:\path\to\uv\python\cpython-3.10.8-windows-x86_64-none\python.exe`, + /// the junction would be `C:\path\to\uv\python\cpython-3.10-windows-x86_64-none` and the executable path including the + /// junction would be `C:\path\to\uv\python\cpython-3.10-windows-x86_64-none\python.exe`. + pub fn from_executable( + executable: &Path, + key: &PythonInstallationKey, + preview: PreviewMode, + ) -> Option { + let implementation = key.implementation(); + if !matches!( + implementation, + LenientImplementationName::Known(ImplementationName::CPython) + ) { + // We don't currently support transparent upgrades for PyPy or GraalPy. + return None; + } + let executable_name = executable + .file_name() + .expect("Executable file name should exist"); + let symlink_directory_name = PythonInstallationMinorVersionKey::ref_cast(key).to_string(); + let parent = executable + .parent() + .expect("Executable should have parent directory"); + + // The home directory of the Python installation + let target_directory = if cfg!(unix) { + if parent + .components() + .next_back() + .is_some_and(|c| c.as_os_str() == "bin") + { + parent.parent()?.to_path_buf() + } else { + return None; + } + } else if cfg!(windows) { + parent.to_path_buf() + } else { + unimplemented!("Only Windows and Unix systems are supported.") + }; + let symlink_directory = target_directory.with_file_name(symlink_directory_name); + // If this would create a circular link, return `None`. + if target_directory == symlink_directory { + return None; + } + // The full executable path including the symlink directory (or junction). + let symlink_executable = executable_path_from_base( + symlink_directory.as_path(), + &executable_name.to_string_lossy(), + implementation, + ); + let minor_version_link = Self { + symlink_directory, + symlink_executable, + target_directory, + }; + // If preview mode is disabled, still return a `MinorVersionSymlink` for + // existing symlinks, allowing continued operations without the `--preview` + // flag after initial symlink directory installation. + if preview.is_disabled() && !minor_version_link.exists() { + return None; + } + Some(minor_version_link) + } + + pub fn from_installation( + installation: &ManagedPythonInstallation, + preview: PreviewMode, + ) -> Option { + PythonMinorVersionLink::from_executable( + installation.executable(false).as_path(), + installation.key(), + preview, + ) + } + + pub fn create_directory(&self) -> Result<(), Error> { + match replace_symlink( + self.target_directory.as_path(), + self.symlink_directory.as_path(), + ) { + Ok(()) => { + debug!( + "Created link {} -> {}", + &self.symlink_directory.user_display(), + &self.target_directory.user_display(), + ); + } + Err(err) if err.kind() == io::ErrorKind::NotFound => { + return Err(Error::MissingPythonMinorVersionLinkTargetDirectory( + self.target_directory.clone(), + )); + } + Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {} + Err(err) => { + return Err(Error::PythonMinorVersionLinkDirectory { + from: self.symlink_directory.clone(), + to: self.target_directory.clone(), + err, + }); + } + } + Ok(()) + } + + pub fn exists(&self) -> bool { + #[cfg(unix)] + { + self.symlink_directory + .symlink_metadata() + .map(|metadata| metadata.file_type().is_symlink()) + .unwrap_or(false) + } + #[cfg(windows)] + { + self.symlink_directory + .symlink_metadata() + .is_ok_and(|metadata| { + // Check that this is a reparse point, which indicates this + // is a symlink or junction. + (metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT) != 0 + }) + } + } +} + +/// Derive the full path to an executable from the given base path and executable +/// name. On Unix, this is, e.g., `/bin/python3.10`. On Windows, this is, +/// e.g., `\python.exe`. +fn executable_path_from_base( + base: &Path, + executable_name: &str, + implementation: &LenientImplementationName, +) -> PathBuf { + if cfg!(unix) + || matches!( + implementation, + &LenientImplementationName::Known(ImplementationName::GraalPy) + ) + { + base.join("bin").join(executable_name) + } else if cfg!(windows) { + base.join(executable_name) + } else { + unimplemented!("Only Windows and Unix systems are supported.") + } +} + +/// Create a link to a managed Python executable. +/// +/// If the file already exists at the link path, an error will be returned. +pub fn create_link_to_executable(link: &Path, executable: PathBuf) -> Result<(), Error> { + let link_parent = link.parent().ok_or(Error::NoExecutableDirectory)?; + fs_err::create_dir_all(link_parent).map_err(|err| Error::ExecutableDirectory { + to: link_parent.to_path_buf(), + err, + })?; + + if cfg!(unix) { + // Note this will never copy on Unix — we use it here to allow compilation on Windows + match symlink_or_copy_file(&executable, link) { + Ok(()) => Ok(()), + Err(err) if err.kind() == io::ErrorKind::NotFound => { + Err(Error::MissingExecutable(executable.clone())) + } + Err(err) => Err(Error::LinkExecutable { + from: executable, + to: link.to_path_buf(), + err, + }), + } + } else if cfg!(windows) { + // TODO(zanieb): Install GUI launchers as well + let launcher = windows_python_launcher(&executable, false)?; + + // OK to use `std::fs` here, `fs_err` does not support `File::create_new` and we attach + // error context anyway + #[allow(clippy::disallowed_types)] + { + std::fs::File::create_new(link) + .and_then(|mut file| file.write_all(launcher.as_ref())) + .map_err(|err| Error::LinkExecutable { + from: executable, + to: link.to_path_buf(), + err, + }) + } + } else { + unimplemented!("Only Windows and Unix systems are supported.") + } +} + // TODO(zanieb): Only used in tests now. /// Generate a platform portion of a key from the environment. pub fn platform_key_from_env() -> Result { diff --git a/crates/uv-python/src/python_version.rs b/crates/uv-python/src/python_version.rs index 30dfccecd..39e8e3429 100644 --- a/crates/uv-python/src/python_version.rs +++ b/crates/uv-python/src/python_version.rs @@ -5,7 +5,7 @@ use std::str::FromStr; use uv_pep440::Version; use uv_pep508::{MarkerEnvironment, StringVersion}; -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct PythonVersion(StringVersion); impl From for PythonVersion { diff --git a/crates/uv-tool/Cargo.toml b/crates/uv-tool/Cargo.toml index d01a3209d..210c17c00 100644 --- a/crates/uv-tool/Cargo.toml +++ b/crates/uv-tool/Cargo.toml @@ -17,6 +17,7 @@ workspace = true [dependencies] uv-cache = { workspace = true } +uv-configuration = { workspace = true } uv-dirs = { workspace = true } uv-distribution-types = { workspace = true } uv-fs = { workspace = true } diff --git a/crates/uv-tool/src/lib.rs b/crates/uv-tool/src/lib.rs index f85075ea6..ee80a2854 100644 --- a/crates/uv-tool/src/lib.rs +++ b/crates/uv-tool/src/lib.rs @@ -1,6 +1,7 @@ use core::fmt; use fs_err as fs; +use uv_configuration::PreviewMode; use uv_dirs::user_executable_directory; use uv_pep440::Version; use uv_pep508::{InvalidNameError, PackageName}; @@ -257,6 +258,7 @@ impl InstalledTools { &self, name: &PackageName, interpreter: Interpreter, + preview: PreviewMode, ) -> Result { let environment_path = self.tool_dir(name); @@ -286,6 +288,8 @@ impl InstalledTools { false, false, false, + false, + preview, )?; Ok(venv) diff --git a/crates/uv-trampoline/src/bounce.rs b/crates/uv-trampoline/src/bounce.rs index 1e90f035d..8d658bdab 100644 --- a/crates/uv-trampoline/src/bounce.rs +++ b/crates/uv-trampoline/src/bounce.rs @@ -78,7 +78,34 @@ fn make_child_cmdline() -> CString { // Only execute the trampoline again if it's a script, otherwise, just invoke Python. match kind { - TrampolineKind::Python => {} + TrampolineKind::Python => { + // SAFETY: `std::env::set_var` is safe to call on Windows, and + // this code only ever runs on Windows. + unsafe { + // Setting this env var will cause `getpath.py` to set + // `executable` to the path to this trampoline. This is + // the approach taken by CPython for Python Launchers + // (in `launcher.c`). This allows virtual environments to + // be correctly detected when using trampolines. + std::env::set_var("__PYVENV_LAUNCHER__", &executable_name); + + // If this is not a virtual environment and `PYTHONHOME` has + // not been set, then set `PYTHONHOME` to the parent directory of + // the executable. This ensures that the correct installation + // directories are added to `sys.path` when running with a junction + // trampoline. + let python_home_set = + std::env::var("PYTHONHOME").is_ok_and(|home| !home.is_empty()); + if !is_virtualenv(python_exe.as_path()) && !python_home_set { + std::env::set_var( + "PYTHONHOME", + python_exe + .parent() + .expect("Python executable should have a parent directory"), + ); + } + } + } TrampolineKind::Script => { // Use the full executable name because CMD only passes the name of the executable (but not the path) // when e.g. invoking `black` instead of `/Scripts/black` and Python then fails @@ -118,6 +145,20 @@ fn push_quoted_path(path: &Path, command: &mut Vec) { command.extend(br#"""#); } +/// Checks if the given executable is part of a virtual environment +/// +/// Checks if a `pyvenv.cfg` file exists in grandparent directory of the given executable. +/// PEP 405 specifies a more robust procedure (checking both the parent and grandparent +/// directory and then scanning for a `home` key), but in practice we have found this to +/// be unnecessary. +fn is_virtualenv(executable: &Path) -> bool { + executable + .parent() + .and_then(Path::parent) + .map(|path| path.join("pyvenv.cfg").is_file()) + .unwrap_or(false) +} + /// Reads the executable binary from the back to find: /// /// * The path to the Python executable @@ -240,10 +281,18 @@ fn read_trampoline_metadata(executable_name: &Path) -> (TrampolineKind, PathBuf) parent_dir.join(path) }; - // NOTICE: dunce adds 5kb~ - let path = dunce::canonicalize(path.as_path()).unwrap_or_else(|_| { - error_and_exit("Failed to canonicalize script path"); - }); + let path = if !path.is_absolute() || matches!(kind, TrampolineKind::Script) { + // NOTICE: dunce adds 5kb~ + // TODO(john): In order to avoid resolving junctions and symlinks for relative paths and + // scripts, we can consider reverting https://github.com/astral-sh/uv/pull/5750/files#diff-969979506be03e89476feade2edebb4689a9c261f325988d3c7efc5e51de26d1L273-L277. + dunce::canonicalize(path.as_path()).unwrap_or_else(|_| { + error_and_exit("Failed to canonicalize script path"); + }) + } else { + // For Python trampolines with absolute paths, we skip `dunce::canonicalize` to + // avoid resolving junctions. + path + }; (kind, path) } diff --git a/crates/uv-trampoline/trampolines/uv-trampoline-aarch64-console.exe b/crates/uv-trampoline/trampolines/uv-trampoline-aarch64-console.exe index 3b7f76564e6f12ab5ad204fdba1c3aad9b9d5637..5f2d6115e1d1df4353a9ccaba6553f887ee55d06 100755 GIT binary patch delta 17149 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 diff --git a/crates/uv-trampoline/trampolines/uv-trampoline-aarch64-gui.exe b/crates/uv-trampoline/trampolines/uv-trampoline-aarch64-gui.exe index 74080d4db835216ce54afd07aa4b8f840cb699fe..3a5a2e34852ee4b1e457df7af10427ecb9210db8 100755 GIT binary patch delta 17801 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 delta 17034 zcmeHvc~n$aw)eUB7H|P4D8@pq0vw=lLItU);TGk98CrrQW->sK2oYtpBN^T;ha@W9 zd6t*Nw3+~#PFt1sb<$|JydHu{cb;kUy2T`RlJ3_j(3nC@B0-cZYJIz0zd?zO(Z zzP~=#slBtM3+3fD_@(>E%Nt)><) zW6lu3HyINJs?h&kWb8OTE$BDim28|aw2QGcRCKx+%i)5(7;AkIb;><*lH%mVY|CS4 z8HZm5eui7U=*z-b$&9+yr46M(IT01kKqaeEe$P!!{}DBMby;7++Av z@Kd>6N;W@L$>O7xYCbOgr4a-1iLug*y7H>(vcMu37Q=Y__O<^9e;>y$9ixOA%#UO` zRL13=b}+WHTQFOD1f!>Hkwd)^CaD8q_MsAHK0UDF_wE~!OwC810fF;e5|~HS$1S=! zF2=Jp-=Utt_#2TJyW*XLPG(ldhD_$xC|M!P`M<;~&xPF1e;BX47m|{DC*zt%an8c8 z1mT2F5Ih6GX;tM9y@J_O%tiLyv8icKi-GkwF=t^D*R-dK8Jc7+qVGo`tgB(u4rOX+ zmi#_rhtr=i?0q~`QqPZNYHvJ#g6Q!I%yp)Wxw`S)Yh>yh@vPY=NF83R$#*?$UIWkE z-Z1+wFZliO17poYH^P`o>uLi&tnp1R)?iM~vx%cwb2qI==S8PbeEJ6Zb4*fGm@}d< z6~s-#uS1z0Hg&WU!@WS$E%2UVr7dh8m!fgMf$(DP zKb4N~naLu^HHwl-Vl#8bzV|s+%b06YB972g47 z^M(oT`$NFf7)ibNe#yO}!0rzJ;>i!=`=wp~nZjJQ(c+0uL)qaxK~mo#=A}Ahw|z_T zkT_b>l+qi@=7nc0P8Z*mY$TL!A;yz*hPeoNZ-ybAA8U8#FxKo1gFy9meyrr4gF4YD zsa_D$4aMk&LR3VHO)Ob*ld{{+u&xIk%rzt=dDykE>%e7%;QaEu88*EjhVnHpBa-lzjE1_5%2f!Ce8DL|6&Mxi_2{J zY>&X2m7Ex>a&BsL`-QPiA!V{AnO7+AE@_(zh{Nlf2(l4MAJz&xq zEIz;_QX#>EU9|c8j0)}_#As|vY9#uQGa`ps0!K6x<%UqEw(wBsuoljw{3t#_zL7*; zna6I!c?QuNhJG^POoFSO$0*JjP0oOJt208ZX9X}^Mn4P8Pk44z57O3 zHkGGBTl(SgUqYXH*ya{+y_t_|qFvy!raf_<8%dI?o1b!Ge0bDlA;F`fom{L0YGaib zCr;uY2vL4Bv5>C}QO26HID@j-ydZ81*VO~W#Kuct!(|XI+8oMm^91fu<%l^gWe)40 z#_o5SOAK+ql#Cwo8eYccYP=73D1LKX$T5@atV1zbl17I)7rr-p+a9g)r%IkBBXL7u ztM(eD4)Z8OZRaRf#Dt-F-D8#KEXAB#IcG7;(9w4=svAt1brn{NzWF@M?inkoJ#fXv zEUs@`2rKe}4C*tL`pf~o5LVl1U`5?c4tE&LsU@1(lELs~%S6{4s1w8*)7@m(yzG21 zmPAQQ@NeN%GTtlxxmd&kam%PM(OwoUmCVvU#g^QC^N!L3FKe-FlQktH~E=7 z{JzGwQZsd7Y)PW4r;KE6w)%3hYhpM2aE$WK)Xjz;gB&G4?G@fJO8GKva%?l~<1)lb z_LdEmA!q-IlALZ#EL1WHJzxmre@Y+}`2U@GCF#l=(^9wv%AcoAiH?JN^CAij z`iP3rO6c^S*foRx-J%e0)%Gzi(aPDSsF#%M)8n~kl-L>bxcSQ6Gg2oIV=gV1+#=$& z7GCVz1m8~k>t*O_y|R7ARIK8y8IEuhY~qd0k}X5Ij!|P#a=#$idc)W}_b$l>Gn(gq z3FYCi?Aw&ZGZ%5a%8r>PnZf#q&HiXsYe!^w5MvP9XU~JuUrB*u;B!x#|IXKzB0|J& zq-LR>BzWWzavE_6Qk{hYB;JaC634Qre@O_-0yChsah_v(I|XFahY4&ht(W%N$AT4+pa{IH_^1$mV8*NR1f;!==W)Fu>7L;~NB{&`vNG z?F19iE{Eb{0y+TRk<7Z%?QjNm*3}0UwoaBdis7v5Pa$mXLBIb95pw{F?2FG+!y*66 zAcEp-2~xWRqEg@=@|Z>Av=8AhDcB{1y^lfZElF%%pgl=zr?wK*%Oxh3r8T^1y4gTS zhcYxe6WP3$e+d1)P*&73hV_$591oQm&AqIa!=%R}q{a$?WgX{H_d8Zw0w-;*wJ362ba=;dFzz+vr=*fSj!bKyoHbLyMnQ= z%zkmturNXPsKNx#bs&M-;c#ZQitF^9P;)?u)*c}qVSw4=6mm}w!DF{FmPLIL&}gdJ z2?pjma{BC8(#U$VkE=a~T{LZ2{*ZPhQ|60%PD3lM3JKO+!PEywS0oFvsqa7}RvyV} z{SdVqhUwEK)Q#_6NGKQF*@SOMVdrVy;(8IBfI-Ks!>YQmeFW{P7G<0~HP=E?qV^_C zNVrQO`CdqwaMOW=zRhDO$u%edm&V>~?doBUNwAmtdRE!}p%HM3G`L=7ydD{YI#`G{{-Tb9y3+$9Ot5yVr zgysYskH{lrreHraN%?U8gM5@jnYmyx|Kl6VeG9D7hDg>-^6z;!;48)J%B}^eoT~h4 z!F}cf2=NJx0TiNBjK7%E6t3q?giDNdC>aZDef%^?6Xde3( zQG#TzlJCGr5FxQt8}RULu9O0tfq^g&2)E%JOyL@bLv#ugn--!@>-Y29fY|6s5t%E8 z`XQpHxSTyJxub%UAJ+R|@|n~p;#MCJD372Jh(=I2354N{hTV%c8#-c$o{zxgAj47^ z-$VqOg2ZU6)Nw$)D1~Lz7dQA6=f1hPzh`1$yeFj!8;>9}PXrKb=8ZV;#cDB4Xb@rk zY^*L;TV3!F>+cV6>pKVwW(N=+B!tX!x&OQPzC~H%)<415Cjg=Hqq&Lly9r%IP_Km$ zxedSHRXbGMREO&SFqZNhPz|%^Dt-y64waXjg-PUSf6dflL%_KXIz-oa)YIt$hpZSo zGoA33g}90lUk-eDuUqDm`Uar`PtM-E=NcUFW&EyUJo(gC_)ZxgSbZ$m9A~wl0%s{J zBCITiZ$yAupXbffLU$^Y^CTB@{3MmyL+lo>tMI9DL45i<-04A#+M1zU}E6a@(&)Ev(Gk6|yqvXrbN z-qQ;=00*6&%O=)#gF7Q96AZ0|H{kcYk#?2b1h%On-xJGh`Eld2ePwwaq8#VT?jCDb zUk37jMd?p9O213!10a-;64Bjg_dcKRem@*h?8;F0U@@#bPu zRKkYl`5;R(9cPlT;_*;Mlcx?2ip=TVv35M=Jm+i7vFMO#S}n_FRI{aYwtjAH$S zBNg8jsjSPsw&G~6MXKEf1r6u8K5FC9CZ}7NYdBYef`s?+{ghgU=~Yc~mBu&=r$C{C zN=ah!!L-Q4@}R6?1;aQsYo%uc@=)4-sHf&2oB(qvye>8=bzvXy5nIG z8H@UBaK}h0#`p%{BOGBaI}=Y_hu8B}uX2;p#zJAQ!LJO;1Gju}P^OT#m8_ry+N_Rr z$5AY+#bNu7J5K)CU$jk&Wn?( zE`vg8@;#aA>c-8$9^hffda)s^i$jXyC6xadg7oTv#kS#rd)?+lrdh>#;O_k|Nmiw( zV+-t2?=0{>Kkm@7+R;Pa=L-(`9K6kk4(A<9Y`u3bv5B>J4-G6`VjHd(-NoTRVBEf- z+qh$a?;#vF`gq&_uJ;I5elQ|UD`wPYzTa`aJ!OLlun_5xsrB`5O-}E(-Y~-<7H&V}jSM;DEei!Jc-LDrwroj2d2*HY^Wv1& zABb{W9xJ+5{h(VG4E^tdn_jG$lyd;4ZoVe;AGF`9>qec$&JJ_FmEQ0d;R-JXobQJYFE1k2 zn}ML@_P`m+0vOPl@$j}up1ZX5!vMg8Q6mhXb;w_2o{WPV;J`PMI%mj#_|ky#!{r$= zdJd2WgXg+LQOE(&wwF>;VIi95dKur>5F}DyK_Px{$tmC>onwbCf>VG__;ym1OT-Sa zMMi?0amt?70smp&OiAUq<~<6r1ZC0SJT(lQUt#Xz;~|~jdUXOcK7j}?T&Um7sYb+V z-UwE79y+n?`sX?4;bJRk*ApTchjP8hH(X_SG_uA3)J<4-OEQ=;u?nsQZrsF$3F~XW_UsUTvqT@W) zR-CM{zvzw1$B$w>!i4a(rG9@AMfY+v>lZJ;PhmU<_mNz}x%g?$!Y{E+^kVFBgeb8W z(dhpTnEW=U=03pso#*{U5lYlOsr-)VO7=Z@@H3ZHiVVd+;fmf4ieb|Fwo#4go7}xf!Gd2HSs85^(fy8m!5tTcSGac_Dz>#9UK9hRR93+BK)a>Ru43rs^BwHddoKDc74 z)7B3U_D4AK|3Ulk8>`$DOZW9j>IF;%TebH3qus+Z1^1n};O;$Rux;n0U;c1(#0kH` zRJnI1Q}2slhX-KDjn6r9ax<`hhO?qdqjdD~FsWm1605EHtK=@(a%@5GUao_L_2fXF z+nQ-O+6y8mtWD#@2od3pcZ(rl6yOi>W*Ek{AN;uh+-dQw3s)_*h)Q>^?Fw;;nJs(3 zOZj3l4&Wn0)WAR^F3ilQ2_eNpwAl;**}s#I+iTZ|I1ewAp1J6#H??zNZak~a=U9gi zD^?w%ZCNHeN?vXd&V_B-wBRb7%Qa+EaL&>!_Z;|-FdAHxTTI9V#L(HP8xzpcbmS^} zV5D_4DcDGtPkC6CrU4Q(0K~0D8%3U{dstW8W&e?G!Qtj%TLPqg@7J(}EePn~CYRH9 z*fOZFc*p`Vnc4WhcVNUm8I53FQ#jXY%e|XrQsyii+d(cjzAxojc=1uJUqIb?^jlV0 znUnhrUrQT)cvo>w?h&Cj8Q)@eagKQtU%P*czld(bN&yovNp1+2IU3*EhFKve0JWLmZ_AUtpoXKE&pK2(ktX{cxrf-umIA!>~`kN?Q)bl(5@B zqOY{+B=YbcKRktboS{DrMi{L73U!nU^I;Q68OQo7(7zQwJVcUX!lSTq zIPy1i*>Lc<3~U`h3`DWAjC&4jhDF5MONf^*BbdMT70%+z1$VFcR*=w!K8ymoPco3m zcNTtwV2)f^Kl*xcaA-x8N(7O(0wFYrO+{QqhwMZ=86fNAFt8E>cH{1|d&?3xb&N(^ z8njmA0_QEtp%eglWao3IzJ~Lr?GUctx}RrFRv^z{o!@>1eR=`0)7Q|c3%CcG1+l!1 zCk~y+oBskQ%ddsep09+lo+;;^Jk8mpSu9ejH@J^zHXLB`U zxPPvTxJr&c#fghhQYTU!vOY{eQlldnPQ=zLkSgW+T1YxLdQl$mQ6i`sN2a$%BK>3p z(yiY`+IQifBi%6q>D`31UQ+js#QAX6EkeJC^R)agkZu@(GzUm2Zxi4Y?pd+Y;5naG z`vq2b4(cHG=c#n=r<@PH!cvzr?RC#K5R;6IKOIV6z_)~NiuFmHGFcBULe-sU2u?tW z1o5jtCRQXlBQi{bJ2(cNEQw}Wbk9MFa!v7Ll3k^tczXeYwQ}op7Z-w^E?Aakxa{yt zMwBTPOX4Ek;-j5~4F+U7$>eEb%5sSJIMlNgQ9wS}0t*FnqHM?gAkK?PacyR;!FN^` zR*;(45y*_(>MMrC%*N%ohEJ>v=+CkdW8vqTKc1uAg+S+!3+|S2uLd)vNpkg$NQqik z;=3r4Wu}kJ#{K=zGI_7&jPfm zBdlT=I)DskqUUc0VlO3-5rF?Iq zfcYhQkQda=FVV9Veg50;m*a8tAjLV!I%m0(p*TCx$@jdyu>#J}w?c^ftE=G2(-Y^pD&WvfwaoQ4Z|vJV=r584 zOpa>_MDP8SOT(Vt>_j^I!BpI*LrX6EWnHyA|68>@eb5TcIAcAVq5FxDI_<5b9XyA# zC!&@%^cksgub@0q<${{9V1y?4X$-Ut@0F}xIw{g5v%mrJQQNU1Z&?DK2(h;iynRNQ zJ8skVJtDOm9V!Ke)J{4d$FasV^tW&bioq%0uMrqOkH8o{@*(u6er}v+E@PZXk;nsq z7X)Pd#D#EhQl{5ZWQ)EGM zLvqx1g6$4%6Ah-G%?6~4;Ma&4xTk3GCt#!QLnZi3DC{n>C+d$xD(-gK#k2SpH#ve^ zl@;3*TpPSg+DsR?eKK#+Or$wEHaxd$wJnztz07rUfXqxAVN%B3UCH( z&jvVg0vBx@{N(@$yGiH?>BR1xBNYGf# zO|8y!9mB;-F<7RRU!JK2eh&r9mRT+d=T)uklWE|fv#==GRx-^+kCL>NZ_K$Rp4V=Q zM^;ar|KIYdyD6VoPZq?pKZsB&nmqz_N#oq$1=EVm;IPl|Mfmm)HjE*yK*0-cQdr@V;(Lz*+27u0I^l zy|2Ws%HVb=_Ej0Yd|p|<>h|={&IcZRX*}7N?rKB4&<)|zt*=&BW7T;0cje<%sY~w+ zqCc1Hf_WWoi<5G4w=wPNou&$0YsGmA(NBP-Qu%g#H;1yW!^l=@-1R0axsSxgE!Jly zQIkQ@F;6U49)Dyd_l5G#BPnV9y|>=IX`~cQca_4q?E-mr>_DG`OZ1*?SMmMhyL!p= zzx#go1B|x-sp2f2z^C1&v=k@WE})-;#M~FSDXQe zX^TTTZeHpDA+B_+>mZ&G<8e;Mu^_Q+oQq}Bv5l!cT3BNA zoI~c-|1MrYq$=N5#VH%tT4GByK9EV9t)HGFo-o&!%AvIh+>e#_*3O9gUx) zQ=F?fSNNFTyhZgZ5mgy+m%`@ZmGMbZk_WNEgb(vnTdG63v&zH`D34WHqrSnk^c0f< z7SdKhiCXuKa+1IF%k@Y3=_Rw71f$UYR0x29i`u_^8G-r&9ad03U^XZYi&;tFF7vvTWw^ z;AeGBd0lBkW%YW?W2Kc<<*O|X)t0jAbv0Gx4ds^l#`R?tb=B*uH`G@(;_8Zh_FMq@ zojQq%r=ft)d0yc+Ea6&}WgAv1zuQpMp7g`Hq3s1v7jSagmO%d;9WK-1Lpofm!>4rk zf(~1Ccu0r8(cwuQ{#}P(>hPKlqqYX-vjm{bX6O}m9j?(~iw=+K@Gm<2n-0(F@M|63 z&|&PefeBJ|n5o0tby%pw2Q?@&wn4AhqC-W8ZXNz!hwtm~a~)pL;T0Y7I_FtLG!vpk z>;P=E5XP}^+|Q3kVJ?11@n}CNBcs0{!`%pD2k~<%vGN;SjdDyLm=ydoIK&@p27U_^ z{6J{oibX5_W}22-{8Rh0$|ugW`+rM=&+71!4h`g+tvbBi}tHt1zG zgY9S2Ff*+|u#}NAiaAQL}tQL(PT;M*(AhrCOTHflk5sSP!R&&yDBLaLSbDCdz%(2NaZ66_hqq{GfDQ zd0t~+Z7bO&t!+cq%_fjDOm%ma);BDsosY4xJjx(qLzTt@LV#Z5Zq){mVcGdqM^xBX zudV|pa7D~7l>=2M=9ib&Ft(ODt!OB%YuHe;Z2e=^cD621vs!03Q>a{2zOIJo#Z+f_ z8fmYhzsLPj`(9F44wjtPz6;9(El&$~Z>Vah%xi2YFRETyxw_m@QCi223FGhCP+r$a zGu5rrcI}&@V^a*uBhSYrl@1J7H?$iVX6xdJOVXitEhfN&8^9bsbvIPpP=%JhMTMY%CNGuvSCSe-HOWfYpTiv z+Qkw=R?2mi4dsq%O&3@g z%&SIU3tv!PzV>D#o5UAX*Wc>QF7P@L%hp4~mXZFmiH+De>cOd6MrZ{gHClg^$wb7mB?j5~GgBQEeQ z;qM0m{bng2Z#VDS(XcQY9XP#*am05T5LwXq)O+X7rD62M5r~eW&55>9W%~|g?u#gG zL5V-T_1qNnG;>k{GFYs0(H-aTZyHT~0DWki?Mz$TWdJ_K*_0MKCyXU@mMQu?2<&Fd1&0gacz`eYQ z_Lyi)0k{b81Hfj$PXLRIjQIdB0Dg*be~4kM955}Gv3meF0yc7PLqR+}AZ=y7L=AEu zn3${~j7ef_2ng~K#%yP=@h1Td`|!u=c!iJvSYl>u69_wLVQdo0cR>b3*tQh>JtTKf z=uBm78Qaf`li&x@er*bsQ@BM_K`hEXzyiRdX^j1tP2sE38MCui{^#k~=}ORW78O;1 zf5#(FTH&M&#*#rP;%>GP^@zhY&U^y673Hnip-P#BI}6y1^25`iNPr7wfm49TGqnj= z!fYbM!Z`sa0N*~qOq4$W`~dx8Cj zW&uQquhl2m7@LOjHb6U8d=an`aP@7BJ-|}=-vUnNrV8HqAP)WJ-VW{o;!jtwfK*N% z=Ku##k+uYr0!n~S0~P>o1-t-Atem@)5L>v{0DppVL4JT!PC%0JNgZ02L6>M;IVJ%d zw+1T(oTS4Xz)cX{QNVQ8Dzi)Yn2Z%BR0LRl7a$F&0`vi%1$+gvbyh-0P;Rb*ZoCSm zUB_4=##sPufCV*RC+fEXz6KG#23QHgM%6;`F>ZAo0%5?{06PI6hCBc06%h*dL*&%= zwnr(~4X#;{?PFfP9KsbSA+OF%KDz(p{!jLM_m4Y}^Lowe8()9<_14$ZezxsrCw~T% zJ3*pS`fBdfi>*VgtW9hi*Ot&`X-jLH)t1vHwdJ=Z?4Py&;{Kui?0|S++<}_*U%a}Q F`#<)1*hK&U diff --git a/crates/uv-trampoline/trampolines/uv-trampoline-i686-console.exe b/crates/uv-trampoline/trampolines/uv-trampoline-i686-console.exe index 3fd1e0aff3e97aad0254437f156ded1d6bad6324..bdc225e4d20f7f55c993b32e7d3114f8b977d563 100755 GIT binary patch delta 16970 zcmb_^3v?7kx_|ei6DDM!Cy+pd2oq=^C_)C3kdR53KnM{73?$@b3;~k}VVcGOj!1D&OP`1 z|D03luBxx9zWUzvRS)h~o%>1MlkOpJTW!m@@V|51C_aWC$Z=Iq{c5RBUHeqesK^UC zj@zsx@k4KKUdwR_fNc8($L;MKf@h+e<4!!4G+bH9>(k%34=qNNDii_bmVYLWtDe|c zv&6B4;{tK$Scx(OWvTKyKaO9coZ`nRV|0TQQ8$EdR>tba@oSZNx?uwe2krz4P3YOS zO6TQ=6iwy0qS6xYBYO5BJD!#4?8h#t6-uQrS+~ASc~ThbdZmox4W-Hj{fYBd?2xE@xAwTcrB8*SjuP#I{W(yCA~Sw&bk7F z#5a1pB?1}@Y(W=0%gV3&DkUS!^fgNYJ5y>?#{k8$a_>FDl?dMX7MmyR=CL-L82CeJ z2|L9;XgTRfe{Ri_R!$B^H^pFslcsPS>kK6M3_0p(ytlMD2K(j*+5V*V@gML#EHC3a z`#7M(`dvnLxoUBTH`d%{wb^WHcdznPWa4c3g1+UjV~{sbGKg;!0vSQ_ z^zF*RJw(6pXEXOV=C&Mm>V3r$CJz|JH{SO)2&n0qy^B>`XZ?NkAk10zHS-tQ)bk}8 zDP{>*W^ueDI-3aLcv_5MvjsLaz=(%fE)Iw%u;c7wUtxq&6_vs_DZh-mSGSi}E=0{K zT!}&K6#rvDU?%{C{ev|$koP{UXCGunQfuEHKw77_RM#zbXa?D8SM z2c)-3?-PbcPylg60`272^Jn!0ed{8;hKc^(eCGlm-oVYx`O8I4+i?7%w#&_}q z?A^hjxTA~z0t*%>Uk`EetxEaOS^Qw-*`W_djka>{){Rz%B){~AO#|77sO#DTQSPH{fn2n5d|zusd%)m6dMiE1UXhAQ-GJE5DZrZ9d74&tUBkvDBY} z!GGk!@nC#4STB#|SiVrzVb_&GI|u(j#P2vphDy)AWk)pnEyi}IVJG5_f#$Xdi|?4E zoEw%hsI+90PV27%O(IhNq#vXFYS^b0gJ{`> z7A%lWjiTo%{B3CyH@*c_NcKROgHeKfAVE`LvM}O~SppzDzXyax5as(Y!doa!*c8o< zxW8(hFF8-PyT=vjpx6$BQk$!5o>Dl3r}NjyKIf^}sw~J9_mew3L;*f58c2%O1P~Q=|V~ zyg=(ea$8+tmiqHS$6e$Dp+XbL_-A&AGdgT)I;{l}G+-siY*A_q9C4~!rjJM)E~omQ?hN(XTNXV6F*Pj-=Aayf_( zgB>j=MbBN=FzW*0ATM;N(O5?Ai4r%rn+|pKi8};+4PeY|?i>y-6W$jdr%G9Enxu>S zUU}KHSoh9VB|7O2UF;<#EoqeQnX5{9(pp{RMdeJ=e~zfnrcE0lAe-Ad%xxX8bg`O! zfdXDQuWT7LJjSl8u5&ojK+C3n4#h7k_g}d| zjG$-$Hsu4g^>51jF{a4#P#?ngZeCW>#*V+K`t=Z??WiUY+xz*C2J~>nE&& z;N}Y?xt&q?<5nO1naCSba?JukXE^U{{bkYEkLBYL8x{B>6yo&-cuTG+E5?qQkb${^ z@ykF3tYuu5iz<|`UaLDJcPS}W?s*O*4W9d5v$AjOaKjdu;nGFHCP9tOP`(|z=dN>~ zg9eaX)*XE|CQe$npS*#ZeK{Bmj6#s6XP4O-I|m_}piT=Y?~fa-Oeq5sW+AeS0w~j85d&LQZQZuHOX}>XYF!Oa8_ve z7qhVYVwbXaVm{|g7&MbyEpW1lv+Hg4$OXap=R$lK3%?M@;g1nLzC5L2yxtX866z+; z0@kInOY9!uUfTl{;c_cztbS_86YoSgm%Fu2jfjxD{!1F z)Vd3)2-n#MJ_iEQOTq0@0jlaYC|Mce{VKt`O3z&FSI;~=kS!Jv&vo_#{_a3IgGFW( zmOEfY&#=qOY>+0k7OGzk=8e~y9*0V*3&TxAsL9SR^H5_sHCBMXi4cT1NG{DrA{^Uq z-2p;KLw{ZGRV=ppQJ35s;T%~fKc<`JoGtF0Db&F$j}&)C$X$~h>2g=4!yMQp_v%(9 zhhRo)Fx06RcebJ585;&R5OBYM*EtRibyNvKOmYsv7#$i!&x0Tm#)_V$cp*Oh$;IhF zqGlf>B|Y4kiG8o$ z{}njpkWARK#NK09LAzuciKiY~6Tj7Mh397&u&AUc7+;AhdkX&HMTys$tX5;Z-fz(Q@r%w^t~e8Ia`-x5ThUMbEU?b7?UOD8apT`E8K#=MQ_~kNpPE>fL*z&jwpeV7?u7b_pbzW7OaYNz`T};Ljjx)~nagkp%XKDpZRte?>oJ8afBP zfoEaqA8cwL{VI&HexRdo2iA zN#SB>6DCDpD`RrKUy-OMgg9*O5Bmm$L4{e##t3>9AipV=+SFMy?TSA@7; zM4P)gf`3+gTnN410AzQB*4q1=xc*1H`kesxd?I>gl8FJ@drDvx5q7--yIAJG3$1W( z?1(*4UGO<9B{t%b4ghzs?HI@16QVQG5j~qFN-LRvU?0z7bBKohQD%U&u?xxY6au7G zdI2DehiP(XbG_O(bhGl>%%QF=S~b>6qiJA)-kwO>V1l3*OAfW_3$UYEGWB;9Wz|cC z&IFp5eQ!^+3qbm@&q9jQEZHFR8M9d4R7?buo`R|JrV^83Me(;#N=8fJRN{s(70fM` z4<-hFd)Y+*g&12*1F&kIl>Bc+arSwT$G1fjEprg(3?K**ZUc=-+DYQ%uKtcn_};c_ z?)DzU>Eg^zxr;|+)unTs1c#idSSOic-u(|AN{lF!B)Q#nUfhh#lyjRqGuruj0WBYR z35l&{NSXLd8`YVgomKCHy$d`6BG$m)N)KW?iL&MiAc!1!f`e8AkLgYD732~sy&;_N zYQXES0fkqixvk}}=%K8OxMQSzt-t8<)S!ZhMA$Sx!hJ1jW2F=!KC|Dme`BjeIwtO% z&)2b5O>w-d_1*>}@-gIiy{iqNqS4!sfWhQO4Bpjoj!aFOz@i#3=kC9OWSjD&2)X0h zo(G$)oX}$tc41OZoO#-7_&>?nR?Y=7BqHGz{PKSQya`8>v&)Or3y4L_HWdTiogNLn zP1`k$(eH#pC2WNB)G>bJ!#dcG7)6 ziiaZ?^EgyLqz?>xrY$f9#EpY-O$g}s6qCm2$_NviT1$9r+c32Bp}ak#fvzS`v?}I zK9VzW_aU>i2<09W%Dk29*~duF-lnBz=lrOcCQfy+Z_p#3?^<`kNYZkO9YsRa#JUkV zIQ8n2XamkeNisgB;8wn>V_ngQ#1@ucYLxRWziFGgIfErCc1(Un_W!lrohyD+^s|2jw^*u`?Ju8;2Y?bZClPcJ%e^P-n)m@f_$I$Mz#kf55N&FmbY2d~4fS zzcO*smffKPes2)krck0A^y1KQAcp#2`A1#w3|^A*@xXQUQEvi7j0eHvD`o1Ea& zZKgvO@ctW21&dM$isZp!7FePjCQztSQ>M9^GA#m)r?@n2nxGkfX;ZOO4TcD?q56$b zR1H|tfG2D!l$LI)mzIFMQ`|i|ZCJcEYyk0TW1%z(7RR))UP^Liz<)__5dvxvEjaeT zReY~r`B?L0$^D&_6PLz!HWu**i~80JoW|@C?i)JiB#ag?8l%*2;omYlS0!pbAOgMy zt`HL#?t~L)j3+z;XCV4L>kbg@wD3ZVQO=pmT5SpQt!vQY^3`6=ml+H$|W(Z}p z-Ds{t{-sJ|fPBHoK0qq-5*;Vitq4xayd%7;1iDIjIELhkO+|7)V29Ktvu~Ima1+#x z8r(x1QvnqsnQ&Qj#r6wKW9Ph90n{#22|7E`)5;o<(>;zU2rZh)X1|5zFu=ATQM7izHX$ zVwd(1yiMJufz1}Y1%lV6FHtR^2!QHovP+sX9$26FEc-YjH1cUn- zTvQx^Bq0?J(dWuKKo@Os8}*0jXkf}qlPAUOgcgQ3p!{|6LKkJ2!-(^!Drv_i-`vi7r4?)}1Mrs2ZB^p1D)3z6f!2SW-do-{PA{(Kv zkLAXie?O^?%-aM%~2l7GI8`!YACCec#G_D#V#=ykX zKsgqE8Vq*kQ{LDe7=VSV+1V1+^B3AC8TVo-PPGCAcvNetqXG8YD|SD#3Q2@O687Yf zs(l`bQ+B4MxhB|vVQP^&!H?19IKet%psLkKJXb?Zz^fWqq6y(WL%PW)AWCJIZR&Ok z7Q7MiMP1BH!C44e( z$X*vsF%WU0E&;2}7rdo38;5C_ zMZ?w8yEr(_+dhdtid^{2)9iD0dVKGyyFjE=aOdNLpbEkdkSW7_l5h999_2HyHt7!v zu^j6OT;fmAn-#mX08I0I<4??H%YftQx8BpLY;s#MN8kdhio3cbuhjc2G4XI@Vla6p!>ajo2W z9YSr9|0*;#Zp69Ao0zli8s|pxX&URIp7E!f>f{bR2x-Jche1BgiE;xpclXn)t=yjH zfs~pOHxf)n6a|@0QQwRMtG7ciPo&4WE~zgf;6w7$TcTHcM!;LIso4D!q4EPN;Dv?r zwQ3XrLsD6;fxpPgjm*q)1-3x1&VW8+(hQB;&A5KYyuchALMTj*&!!fW$v|q;G4kpc z;<|Of2c)AgBoSigA`StLP6>&?FaeV#s((DG9elF@lz_%W2;~8cLm;fha@t9gJLu7( zGCuI=?zA=7=_|C;OdzeA;&0jQE6C(S`-j=bRqTEc6j6uJCNnB_r#*s-D;hQ25w?aF zpgVGkk`hab)HlA@&W@TTG`|N+7=rQU3f{Z*|AKPYpM$|vzCTV_6kZe)_iChi`qRwS zK@#1-j*Qt#1h~}dpg(K4C1|P!Fx6`4z9=C~LFi&N`j(+fhgb)AnN*d|n40mpMdIH! zVzVAiB;O@NsToX+0)_^kM|xR#d){al@dO>o1i-Earrha}jM&f11b11DglqEd)oR`? z5*@%na7wzsW+gzxl2MF(Ru%VubbFQ%j4y-gXqI(gUk*@g`4uh#U|rE$jb54ZfMq>F zSCCAspORYQhC2L(rA-z$Ove!f7Q;$r^$ZMpnct4#*o?;PJmjnq{f08apb8-iVbCN5 zcdfX2(CCJ%@DlYd=w_MSMAz3}wrf$43Ad2T;aM7~FM0-oDVXlpsHq>oh9a1p6LxF9 z$OeQOdnld=W&bt)-A}H|rY2)U@N(YA1aA>(7w!w}(AD8!8IU#L60RPb$IO%vQk{J> zhg^U!@<|s>P4oYgSM0W_$M@0H{k)6G?P*ivB1qi}_J^iEs}2fH{Txj_{GX?OlV*PM z-_Gp#`^>Iere1o>)Vr`RB4fy_5Cw@LHjuT(7IH#%o}LJ}gYgqcG~3mkP-$cpT3*y^ zv?MuK-wd}s3eN%!p-I3lBMdDhSpA(59*(97#wXJpomOO)R*PysSVA*pC|cgZIIuNY zqj`;F4fEEDz}f>9(0EPQ(E+{>OM0e+>(=no7JgRaX|u1yz9?C1v%8}45x|on{JTZy zNIR0%9qKqj`x6M`00dF3QY>Iov{AGvo1F%|8X9!q?}IMml?J_uL1g#4WVherI)Cu{c36W3=;+=>Cs~@y?RZ%YZBO>FLRKgQi7xV!|m_D(pU-RjXD8e={ z9v@kCtM|~nZO*awAb_`rmL0FPi7hP<5Lk*HbpaaWgF@grm~AUr);t$xCnH|K`65q4 zSjevJAGHtdR6PaFx=0bn6J4s}4iJrim$W zTV3%k?lJDi)^4ubx2y~6a?Rm&2?2b+vX|C9gJ2OBR(0Q8nVs1aTK63S(AGs1eREw> zju$D4)b#GGdJWk5^;k3Pnf9zfbf6sqQ^1@Q`(X78T;93l0|tb(V#{2Ft9_7O4bt9U z_0UP(8-;x(Up+0ZyBDSjs!S~NhVuGqG(2pd`NhJaIN)tu+rWT`(1|~4uVPCUY`Xaq z{|j&w>Hx@xeM-gdx3G*$?KcGWj&cK+)bS^72JV>4h^-~4W6=hD4IzI*y9PEAuyId= zYG9z%o3F>E>#drlv01_2x3$-(GCsw%s^ucKFyehCu`?V;8Vk?_-;$?HHquTqRqGVb2)hX z>Vfa@NnD!v{55gC5v_7>rReF0m)tpWZ?p4={~7|60=jbsUMA6V0adwcqUdpbjW0Y>%D+9B^|C~^41U~m(n4xnkluFfG2wM2_8r|}Gj7Eo-@VXB5g3^}NC4m2OG zTbDI)qB9bAYJuh91;UD~!&Hdf1yV4CC8WiO{OI}zLa6S-m6m1$jc{uQ^%bg~g2>#? z+Hs|edkZi*MV$tX+YeUu(mwG5QyG_{y(SpK^KnA{WuY zw@`3i^v@!5&?|@=#@+}9muavDHQG|#E zWT)wne@iIp$Nv5)6gA=VB`p$t6f_ezIPh1C>NfO42M*Ra;Ir-$^;swf66Gf?nE32OBXg`l$am0H!1WawPlN zC42v6e%Pr}^@-24a|L}z9JVao6p{s3>D4Y==^;{~qyB7|fv+$Dw1!xXua(3W9xv?y zz5F%r?}t~HJ-K4{7j%l*1uu)0C};6s6w8NE59cn_--g<1u$^UUEQEU>CUyfVne7Qh2UMHdoKp83le@}OC~f`+=0>H z8U>hf?O*GoJIIYeeB*Fq6&H$Fx_Ss^ZKAwNjOKF9G3s_hHg%(H68 zJ3uphzS$c(er*pOzewDn5hyp4TEC;Oph`=EC+H*RTgClNtepAN&6Aglj$XsD4Sa&} zlmB5&-XA1+hba*^=+HqfkFpU(>Tlpd`eKYYwSfQ!{ zAHbC|=FU?;?9><)N~wK}iWuX~;;-2wsxo^;Un|V8w{V-_qpw9G8l%-H*duH{B0q2r zbCazi(@aK)UeN@Nfw}fY-a!FT=m@ljf+KvNzYSYumwcPGVQLI`9uCLCV4=vU_bsAG zIL!WsK!xZM4$VG8_|Zo;qiWuc9T9~?-&ne0*5Ruh!f6#<2ZdxXb2nT#wXMGWW?OKV zCYLRLKr?KK1ZGPRVMHbm53rA_*!>VG_y!zH5ilLV_8|R-@KE<-C;s7xy!y2CV+0mf zSb)uYK&~M!a~nE{7X|Fn3^DJ7+!HCz_)yIO3f_GB34=9EggTaikNJplU@+nq9{x1+ zSx6t19>I*^$uR##WIDWS1q9{4c7wd(gbys7dY0@=6bO!GvpKpM=4l^%2i!ZVOT-tL-vtqg@&h^ln!~}fjm4n$0NLjFb0N_<;|tO5$JQT5!%GoRCLG(ugh(> znDzZ7>Tpo2%sZ7`BxN#^B!~Pmh@qXp5eAV!K&%qgud!jcxCH+NcyjyDkrlgNBj&%; zAJ_nyUsC1u{>u7=sY6!bva8H1f)aG33a9VxSB@=A)V;D-`F>$y{&nO7pgkMf!C0R# zg{4YR_&7K?T%7HC1EYPBGA{6Nap1qC1!nPDh2V@4%Bdc*3LTfT+Ck zIz;L&a)@;QwJE|f%89e=o^R2xNPPpHWr6AJWt5j+5VYxWdxhVuM5<5V$YDO&0Webt>^xMp<>W!0bLh<{ zXQ*)ys1Ez!irvQu>GjHM#dl0AB4H$n{MVd<`Wlvp4ELxX!e{#;>B@<9@1YSk_2_F# z?4qO*|N80w95_%Ug}g#bM(wtRi$vYvCuHaC0 z>Wfnjl$lL){~O4`a#E6)vgO)^(#cnV0=|t#s%=|*xmTy}`zed^+`U=4dtOpLyLa@6 zA-L1RK|u8CON-Qff21%N3HWFY^FO_$46nRhnOd2oOR*~zmFb30K^pobWh>@Xwp6C@ z=aly=vkEId#Y&-DgA$cuhq6+2-!&q@C3PY$nrWj}NS6tQ(V)2pe6R@%WORyqOPQ{`A#jOKs;GM;myLsjJRpVS6(2JWyG~xFa1=oYf3C?GJ`_C1j zMjJHjBUpbKAEYNru!By@ifg}$Sce729~9Tm!VG+8;2^F&M*tFEr8Yne@I#2Bs{ASp z(Q5}mH~Kk&0q7Y6uAjuU_&@=ILb{GS&N}fFelMh7gw3bFsI9~Vw)?jqC(WKsh6{izU^!FwORfu^tJ;g~3rGdPC9L>SaLNFD&60rctnWpxB< z2m*qU%dp$vfNK18D#I7Yrv3r3g=XsyL5Af6CQW618lBW=m?&I-%p~&l5zm?Ce@1z2 z@ola?S&Ry$lEXvxLxU_j$FZ%t$0Lc>7~bZ$!K{ig>RYFR!9E@Me|`!y!?x>-^*SYS zsl*H0a+eN`$Vmc062Y6HD3Bn|iu%Nfm3RyNlo3XTyKe&ajza&aPUX9apvpw$SGAI2 zth1rz_fBPLT{_Px|5?TZ)N%1dU=D_LBayJ+6bf>{fxp}1iFyn^|2XDR3FiuQDYmxMA2B^zZ9N)<|-!#b8*hnHWXJd5%+ z$`O>mpaf95P$EE_feOE3e#H5;R^=((LrU8UlhpU~R3hJGl&xz~R&w0JqP(KlA1}M> zg(L4ARD_1)g~OLyxk7|-eg9tegnpU&^DS*}qwRp3P8j-h;o!mZ~x6N&}JhEk2P3S|?@7L?af z+E7lQoI|;Q(uER*3xh2G;uyh$-)}uU!vK{3Z%C{(2P@-syDD-ba_&E*F0+f<) zeFdItQJxOh|92Y#o&G=X=zq5%2|N3rcEl-*A07@wXVy0~)fOySQBz;bo#kdW)-G|> z<}a_WEyWz6+MI@^^OxFd?|0-9#X@bi#)kWAo0_ziS!> zd1I4pkMsJo9I!=h%b;ka6z@@V5Q*9slL^PTne zwk3{b^OiiM?TsccT+-y2)!5k32**P(Sl(H$?LG|E`uElliL?y^k`~Rasc8fa2;XoE zY6Z1Rpk`W2kz+}t!@2VAxhv`#!rLrz)PzaR4T`{^;@XE+5>gsvXlQPIeZ&12PVf1R zwIGH)g5DR_hI&Lnscwoxj&Omqw($`~bi~g^zMV*_(Ql@su|Bi_wxqQ?8yi8oyycDH zUqj;~+CqT;w`83WS)^n)4xczH5)i~PTf_1dj@m{dr8Zk=>btR@#)pMQ^#&GQ%>@vf1)qmVK5_EI(O{*|%p; z%C=ch#yK?5`6z5drEY104&a*i$(t$ z;kZ0JmZvnNtVwaDY)tW`Y)&~l`RB=TQ_`o%Q~okVopNK!khC#r=CtW)?ldudT&A4$ z+pO2KI)mq$^w&|ReEl#UTlha(A z(q2k?J#B6JhV&QHUrj%j{$cw0^dHhAruLtjICad_nN#OX{c!4M*yGiyg&8L@lc%Lk zTQP0Rv^S^evgTxUXC+xCT7Hr3&Hj(TXS|y|8n}bv;iAZGf2LyW^`nHknv^4 zvdnifUzpZ4Eh%eX)`6^#vcApw!P0Gs%Qk_4b8@!i{4VEg&bgef9C3Q)^hMKmO@EIT ztw#10kEWDIQ#w5946%&1WLVy}dB*C=X|}YQv}I{)AQwNUO-PT+PRLFKno@Rh_Jr(|?9}WG@ZXx9mtBxum~G20 i#!QOClQQUfwmxS~PIHbs$CI-$=X?&wZ9DBT@&6aAp4Za= delta 16613 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 U8HX}DGvYJ1o_^NIK4;SYZ&;P|E&u=k diff --git a/crates/uv-trampoline/trampolines/uv-trampoline-i686-gui.exe b/crates/uv-trampoline/trampolines/uv-trampoline-i686-gui.exe index 4221696a17659b1fee988b030c0170383c4b7fab..d6753380da3e2a8ec4ff96e47ebf0d3d4acd4261 100755 GIT binary patch delta 17501 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 diff --git a/crates/uv-trampoline/trampolines/uv-trampoline-x86_64-console.exe b/crates/uv-trampoline/trampolines/uv-trampoline-x86_64-console.exe index 5b8fa6acc8d7bd0313340178eae82fbd8c5b47a3..b93c242e7794a98ccab1b07fd2defea233b990c2 100755 GIT binary patch delta 20553 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 delta 19847 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 AAOHXW diff --git a/crates/uv-trampoline/trampolines/uv-trampoline-x86_64-gui.exe b/crates/uv-trampoline/trampolines/uv-trampoline-x86_64-gui.exe index 8cb19cf8fc7cba9d60e88ef504945e020b4dc608..c81d8e4e588e0f63a3b1e4b49ca8a931e4d18252 100755 GIT binary patch delta 21124 zcmb_^30M?YwtrRIjeuY^YP&C}ZG;#V;s%P5mc?oGtn@qM?? z-S4^Qo_o%@=ib77#Na+Roc`C$4dOZ(I&<9er=Kf}R~3#cppqtzbDIEn zAx*IDubJacAzrE^@;&;As2zut`5BH&;@hflT%;FO}nJN0e%{8CJlK3q#nfbT$m-w=YEmKF76&9B)((van56*zFefK{NIlOV(M2=Ge(S2N1>p4!8>k?1f zbuDpprY21Zuid!i398G@#v{u;%`iq)00~6my)ka!sM%> zd{&e%XUWZKeIe@mR#mLb5YllrO1= z6{}G+++C`Rks%r&OA~cYEUTYPIBKjgkf8Pi(`J5qQ~+%rt*DUJRX-~} zncCY|6m!pAM2E+x@uKfxvzXkxa>%vs>md-TUc`Q=IPdH(bb5md_tOv7O?blOzymOC;zhF8<;y7ina89|gD1o;LUC{us)r zvsf{7*e;}lQLd>)t-BAK^Ew!&mMPY5bCsJyR`;XLq0s(tHoZ$ao`F}Jl{|Ad#oevX zh<-p$@;zpr=8u{rc9;etUWX)5KbVV6%(qJNinhto?ylc`u3I$EZ`rL{PV$B>1 z%FXCCh&M)wq0t=!t{Zs571qXY&V5}d#GItRR0bf)ATa$}tu7F8R(2e}-t z=MJUZsHmo3AF2~asV>%zQO&juslTuy>!c_*5;s|EJ<;mEILUny+1Vl5uhvNQb+wJk zwf*|=HOeRb`rH)(;VsrR#fqNe23ml6TMda%+dldYWPJXqmFONL7i zDqr^>Vi7Dg2syPvOu98m2Kz~}!x|^a1z;QbTH@&pS7L2O;t6!P z4Bd3VSk8VZ*iWrPmM_pZcs83A2;NmFuBa`aM_e~AFcUR|y=g|04Y?^s!P5i9w#v@( zN%zItE9Zo@q>?s7b*M};w$^S)GYWe?FML!5~0bxt82Wa4CaqSV}64Il&Y_IL>h~(Sx=qD$diL{!8SdLTwI8_-q z=utz-L&~29eU+dVF+K>#Y(^*62BX!e8;qSaf#x}qR8OLvq;??Z60xe&I>ErH%4^og z40m2tMh+Qn_~i#>{*b{2;i}>pQf`>`gK~4oHbYIO@|QdAFg$%hdDqlaxo}6U(Pvs- zs=OZG-!`Ze_Km9Q=p$eP&(h)TbVm_u&W4#o|X;{!DpJUMTLr&K`!@d z>j+M$kP%f)pfPDUpQ!_?h{?yDU415w)eZ^X#i$ah4fcPHPpoYgc3Fbh&O${taz#1% zG!l$Jvguvg=6=bVp!%uIGw~=gT&TK+C8J+QGUT4v2!PC5{EGgGN05^ILq^{V+^xC5g1D9AqYgf+#t&D+paY5j>#~4)DIv+ zytU4_W2T4D0AeEKa*U^fX{hY2btJK>f1sRCzYB$ePfD=30f&;(vJLox_v$(ky}M1Qs{ z4s(q$HBhCHsRkev{R)1aJ;Vmnvxv|iI^VKP=^sDLkiSfs7T-I54GOTJb7yhfYrOhy zIz*QRTrPBKvt-^$3}L0G{j6+?A84#Dfc@F|&Pgy9rr(*9dCM|m#357DpUq3yfsj>~ z)b(EhZT@o-*Qui1Ta+ISU+h_E=BVua*C45HZn)i-shCIRx{?-qQX+w#-Qa8%{iZ`R zi2^XKYBan#wO8$JMYSvfI&kRe?IzwaD-WCxtpN@@#fYtogIOjg$9yM|9A)z z4@;^UW-!2;U`3bbE0c!B#+<)Qvzy+gmM^9%rNd4o{B$l9N{_gi9`V)MqofVN8}hg6 zd~ERG9c@HIe|EVAvRxe#P>SywZkTya`Qu%&(%Zz?zI$L|qtg~xvbICW;RJUDfUR=w zKwD+zAn^^-KRF2A(~dB3XIQX1SaFoi{rF(c(FtG(CwMcS89uCMFKXs~+=}hdS&I(g z9ESr2C2jZ&{xwAzKH7CDWEnjlqms~A4AUwjG32>vbQWp7Eeu&Up>*Qw7|l7E6nT=} z6H9a2Z$g%BA4!mqm&gM(@GS+N9J$Fn&ZA8FNHmbr>b( z&!jdpc-{lQ(~d3m>a`zfXCA|-SRVy20o6RnVte8tu!V)9v=>kzdUWyvbyM2KV3YHO(^7hAKlWZ0oM$w+w~)(z$@ zf14$%%xd`@x>G&|wulT_&LfUkp?pNIyx-alUs##RaC(R8#I;kv>H|x*o4(P=hH$21lq`-7 z6^sOMWnu6R&asK-ZHI`~i#;v5LPZ6OUCVXCOm1kkdyX1`#p!9FHf@VwkybajpsfvR z&hoTOa!g;mFBh}Vb?%~JjEifKx;Du%)N^fwQ1LhF6q5?erCLruy8gxP@*E9k(+zY7 zU2YW%6-$v$JTLi;1MHz%TJBiFkL;ra%^rruMN0f*=sCY}0kglZt1(|dKlzPoP!&fB zmF0e`-nd9)yQI!;A!4=DZpe2b7t)zHBS}O+8zgU-5W@#f;8aOS2EJj+hW6fCp<)-K)E+DG40?y+CUbEV&JXT;!i0*ai8vKk1n&u0 z9i00B>$rp9c?0=^*=r>;MKEt<*@Ahq^$bgIwW@%kf3vle(((5v2=Oyxh4`svA>Kff zNupv9W5cI3PNXOwuo5DYyxsaT^;{7}6~OQb*xC@d#7kf>+5mykGzrgks~dS3Ln5*= zq@SeITwJ=fREXQiN`;siW+A2{Bn_r3_c zO;j(?S~x_O8P3eY6g`@P^D1tx|f9tTU4O-eiSt8YMl26~|EkShuE-G??DJ1*O5 zD6I~Jsh`x>1B}8ki2@#jpL}YNYbsoVOgD6jh+{H4)Ooik-U?qdYE_bxcIpOw( zDP~mSggCFZVnje{6}Q>?6oXr>RW?K<{|-hcn-KrI1pLg2#ZQ(QKN;Mc)C1dej7z^& zcb>=MuC^vbI+lqOc3?aue_AgWvkt#iH=Lzyu-#fqjp*!Sm(x>VVLeWbJPk>~wTuUmqBA8V7#N?5Aof#finOGfHA+nV;ssFH<=@Jt z2*a4<1LK41>LpN-whgf>7BJFL>KCtzp4uDkYevZAW60wjDfl%w-lhe1rfT{O2fWRP z#ihrS#10a9P3n%@;Fq(7H#%Q*Cr3E{f^E%^3I0B@R`fufa{Yv7Yc>4XW>X8m&4E>H z?Y-7Ug`e2Xbu6_f%yXKLthxUrp97sQh0iGoW_?sPO=^$zc}xzyl3l$HXyY_DR`j$+ z3SI}LeA9WGZ>~|^Y@L9lypeH*-S>ES7UYgiFlY7@;(aq%F)&E|ndE;_sMK8i!8d5z z3+qG730@_nNIqOb_wv*ZGPv`AwS_I$0V^!VTdWu}xd$zIPvl8}v4FtT06LKg8Ok&O0V6Tx_06oEfcRTFc8 z2-xM0UV+}Q$K2Nr37cXNvAI)>h=w5ww}`OrLqv!H+~_5690}c%EXp4!&>*DmX0{z4(kpen6A%EcByr0WV(Edr*U{izQ@){?q|xK{C;awkpx-41=4kDuY|C{~{QZYn6m}oTr3%*e0~S zV+mEbmLCrn8^W+_xL_7WHv$_PJ)FVqE-TXS~bSUwZo=6pjBDIxL z@MlT7lIegM6NPt^$dj)I7GSq4jiVOy>4F0hdC;N0Q%TqKfzC-h0W13LRAuVuEW`3_ zBA{K!(lTAyIi{y!!bs)Bn0_%o!d{1v z3ORCN<-=h$UAa2uLBm^3O5WJXkx!65!&q;Br0f_w(DlkkaJtaxmK_~vbJ^uaQ9elu z@nx-hwGw!X;2>Hb%!3Xp1vu~udplwj$eSOK4{-@mDLhjLp^5tzsK94xV~?j`@tFXD z?ZJA}*`yMN0~)1-xg0}jXAuXGVeKMfl3_}0ae^QgpV2(8a-;PkGAcK-ZIxQN!+L== za*(4C2jdN-II!M%wBDse{zRw^tBnMyi=6{DNY?u|z%iJ*q1bw#`?|rIE&5&HZ_)#? zVnxVd)7R^{518Is66qYtG?N(LP_|IyarSB(EZCTMgh3>^&GNXf;oL^F5;N#J0Z(LNm($ihpPzEmb41)#Iz$I^X0;a#eBHf`Pw=%oG+;2F7&tgERRV^#=USS-x?0--7V0_S+6RXdU#>>U8;a2VWl{ zaZ1G1?yvloK3ct{)Yoy8(u}XGeL0Nlx6IKBX8_Rj;B5yI+iT;tEZv-5;b6KA<1=6hJi5P-o{rzN?lJC z{FZ)N;~wpeW9J8!qUbk=o1`#o(5U!5)>uSDzt_4;=|8@=iyaPeNPH7w4#({;?Q2`u zal4BInbO2_96NC1mLX`%if^ebRJp^_lZu`+Oc|i|7+bij`t6BKiG%bIs~E zanx{+wTjW>08@veyj622VXDt#-fdnQRKfBc5y??QbL|NwhPxmMJ<~1rU!l_PyC2A6l6L-wNiSep*X-cy*Pc8)iJ-2uS|PJnmYQO%r>$UZz3i zb7DN)mfIPG0ZWOi!DXBGbCoIt17AuO+>A5P30d$x<^~2{ltA@@_RT3oD^bU zi9=B(kVb*)7s3^t;+jgorK&);+AWm}>la{6LzbS%#Tc|^6JzjXLh$-9jDBf#8D3Ug zT165TgiX*vN;?ZpY@jlQkaf>` z_xxxqEAq{r$ceZob#vUR5$#2Add4@dcu@2l4i}T(7d+&I!p@mA7CWaJ7^v1e&?S>0 z;Mg9n%4@_0ItyMILS@nUz+rzmRsISY>ZuPyA$3=8!bEc{VCNa*XzObG$JSGn?~N7Z zcuBV5em~!=s)xag*uIQqVcA!qFS>knuFdWHmBBXNTp-l%S1S|fUyjTswdD~^R*n)oIHHs z85g?8jCa$F$zP`aY^8=9wcQ8QH&EsV9NA!BItQWZ!O^HXlyXl}Re$sGPY*JlPb^qX0^z*wXC^zark z12Zub5_<{CTd5=aY11CVd~LYrq|;kPudalY*V@9gi0B*hp-uPMwrP;AZL-XEfPA)+ zd^9*-8s=rTKP_qtrb2q;RWKp>7FZgK$zg1J@+Xpz#xzJcp(1}px?|8}qM3x7R{1iN z1c`=^WWyZZR`j$c0UI$pmFT- zjl>h60KGVglqC71tcugl=-?w+$@LD0B!6ZP)oU(>L960Kq5ATQ?zSs+yyHIGtGpzi zM^7Qx6z79D0kjTshS}Vg_~195)?DYsgNo2=5ITNgeP%ji3+kiZ;Lz z02<&EQmz=l_3IHFw?=qDAK@t);h^Pzn1epTL)r*leT2vK5q>>D*INTj!~jBf*ojQ& zLJ~J)Bp|mM7b(igS-oAC+tg>~MA&Y1>KsDiv``TT4MlTT3A1?&GohkqC(6`yAS6yX z#4&ufYBtIfPb8kkX*h*?ZxC@80Jq&LL9OqlZRF`_imN@%hUD<-1kp2eosQ$Wm$@U_ z4KpD=m6#xv*da9!KO9AH!raIVP$Wb%B^ogkO2p#F?bZ^hrRbM7JridXA;}mE_S_#q zHTDtjLH1~qE$W8x<@e#1iKiK!7b={v5!w-Pi!lkmZ8=bDnb1vd*o+7@%tAxdCE;3* z`=$wun2}WLV|0I#YK4S~8AR0v?F0@fii@Qh_?ScV3V^gsEunbGh+oM!BpalZe8cOA zqWs0Tq{1)la$L-4tFyN$3V9$;+K&Of(lW54B_M539g zBQR9Y{Xd-Fn44k*bFk1pLfJx8Q4gYw^gClk`3JIYRCcifkw9#zZ@|p{=#FhbQT|z7 z_BsT22GIwDQ%gp_{{^Ep8b&YBsCL2#=Yz}YpV$qrj?sGwC>#F&T(rYAkwK_Yza*gu z9pFInpoXk%O(0&}3tT?LxlLUWgG_&P`7kJPP?4D_#90za3qUpF2XssdEEkvb5Iy;* zso76h;To(U`n;qTf6T`3Oz#AUPs?waF(TZ?)RUN8Csce6mSCz<&ByvBet%0NsW0L2 z5e9O^kZReYC4>qKG!mYRJx2=9A00(#jv}<)>w}*fI_i;4gB;iGBf{pP5itkS5<~}D zcD0J?lcEk;Vv@CTR*%}#pPO)XBSN@(R}+gvsB2y2=;tD^1XO`$v~433LFXVBTj}fS zzo2Vu?t2VEMLgP*D~j5aE2@^k&AMk7ONF%L-`=J)a&gZ{zDg1Tl9jXtUZlRs59G$6 zo>7Deph9Djka`+JkO5C=&3WdC;FReZz2+W8(kN8?5fXw-@n&iOW@rJluWcn7l)(>9 z9vU^D1op%b;q=5(s;_1DrfgD`Ih2Pz<((d(yqVF*m56kP|K87Rp6laRMM^^Thr+sk z$iro+&2w$F^WA_zEkY+87GW>%X^9oQt>I+kj1as(Bhtn*%c`kbfC1xg)LVZp$yb1;C}g}tk0-so30q31ucm<JP3-jKmz3K*iv( zF<&Rv#GgHA{IT@y2ENiQ_*kZ$)j9%(Fpk< zCU5OVq;Y;GcK2;$;{gSn_q^qh7wMJ89?E8Me5dn)ximhG1DaDwdp?jD(tJS~iQl)K z3A%{5k|eB|S4ooV$*ZK&o}|qAIC|Z`F|pk;Pm(>WE{cr6$~xi!Qnn@}40i zArzQTiu^=qoLad@yXb{fnSpd&lUaCuqcscBh&r+ELM+{3*paeFoC^FC9ndk4o=A}V z$9uT=Awb}EL=LMv&l15NgVaoen~QBVmxknZ^(kB*cqTwZ(q3=3&EA0>9pEtt1pT`> zVeXGZsk$57p-v}1EVc#1nMhB6ZpzW7PA)7pl`3HyQYCdQI==tymRKQGXcp-P@VSrPh|LeCUK5YbTxSOjNql0gr}bQ z0!fnOLzoemo7EgkcwZ4p_@* zqEOA?n{rwqh~bK7Dbjw_W^d^-60d;&$YI;QwYs*^)5cy@Wywd{_ZF=_?X8u2G;>Ph zhOPQZ`---=vV_UlO*^#Xx%_#S{9~5<9qq02u!-<4b|c7>E?;ng9HB<$vc<-VMHy|Z zQy77{ZI95#ibO8kSRAe~viL9U$^XJ>9v4Vpk`DRXiZ8skJ5N5L66d-Z zvN~+v{$jG1l{m@6er6CjAAwUZnyC?dx#*S*$X$SHKSJpk9=FNwx7mF}bFfJymWo{C z>OWpnn(nhk(z?RYtls;o()s?OOW3L)`SfeR3DS^?$;Mc4iYW2`Y-YU3d;!`Y>k+AT zfomF;N3a9-6XBrMKBvZz%UWZ{S&Xo_=m}#_tH@tK=Ml{dZ%KkY#zM^?u?*{c^vg5~ zUc=4@Q_dx?2%QH`(F|#s&Z$>uVf@hoI%A&zg)BFQvvc2g6HvmzYBu59c5VSwk*2e+ zfFo}+;soiF4mvycYdeUiVm`3tufpcXw_OawwtE6gw3T5v3V<;5zVz=f?0XBtOopLm zINb7-zYZ*MIu|PFGXq-sa_mz4Sq`Frmi`9erhOP9(#x-T9Db6Na}V@&WesE)5MUlF zxMa|X9y?+UI;m%2vm#QQcm*wDv6tH1z3qO6QLH*H&buIjc?Pfr;p~dXd=gIw$AO-Y zpzr-S1afL|XH92%t{Yc5g^KIHgj}KEDpdJ0wvFMYq6(dL)urr=EyB28$u+^7P1YAI9$Ip8nb30UPTGxDH%FP|8+(dY5Z;(2 zfuYP?i+%vM-F^g4eK?x1kG>0uz>g${NV4+m#>nQkL}R;wS4RR6C*=H5CHXJ__w`mxz)>&7tzbEn~MCqp}9E_$~HLP1@GTz zI_!epN6)(a_*Q__>;=;PctZQcMU+FjjgtHhybYtBcusY0Cy6()u@@B$IYod;0;m=B zzU@lX{62=J*Od7Acbd&qv%So)U0F1LjNz?nWy}18;j4jgJ-hk!N3X%9*?w4#oVu`+ zeti$}ZKYf$nk41w{MfF&s@cV;Hw`^;Y;Wbx1-&x|AU|kQzuJa{4jl=ef!yxE zDwu8pHxP&Jaog|G1I?&MheQyQ@{95a~Fsp2Wu>>`A3O?=Jgka*6m&9|2%F)m>;QE_zG!&$bS? z)nKxmy7#=MKy;qmL^b@DziREO^mgTcQNGVhiW&yLr}p{LqXTy*=35MgHwGx;qP~VX z1C+&!UXFeeP6CNO(D7S7=&ST87&V~f&)RJ1o;uu*roim4lUK1q1j~%Ep2b z0=vJE--CUUB_CHalpO`V_pR z_?5fwdoY=|RDxV=Rekh^oE&)S-CNk#Mu(PkzoW0+cQ3w5J?-df_eH=ZZ-+*xejXUg z=YqICW1@M_7Ujy4G|O1VUJmPB@TZ-|MuI=By1z2LXpZ7}5hs|mMj5IwCwD=QwF-qj9EgEpyK z>jMQ*$_Eb(jOh11wY{XKD6J1A^u0uLWA0JIigGU=5MIYH@Y~}aVU03%Y2V1!KaiP^ zZIz>xF74}j?>TCXNnKWRw;&4n?EQpBV0+EC+L{#Ti5}E?__^rQRG%pVhK87YhQ4_U zLlm!(1n<94g}!-uRr}^C`Vw@UP_dAPmiwJSf^nT}`Vdik=|lkK&<{9$c%mDS#BLpP z91y!T5iG}C3Pi1axaGpY^fECEll(huYka~4G#!K4*Gy;CY-G{BWEE<&@0IY$#aZ<; z+Hm{W6G|@%#Q4vIb##cL4??%mdoSD-LR={R;vvI+{5^F_Hx&;*%VMg5NI~lX?s zraQ0pQ9dp1>7p-V=mddbV*bId_wda5ta=0XAU=HxnT}d9pXk@|%f4IMh6w#ZT)c#+ znntHq)PED*(DRZ!ZQtv!m;KS}sL~HL)UmTm8%38P6W%WfYUxh@7`!F`rSLWijQ2d- zPs58KPErgG^De`BlMTg0lIcq3viKM`x(e2^kKp3eoo{2)uU+O+o?2F_haRsjuQ4cU zX&hg(JLch`9SmPpDB~StBabC;sh}9wl%PE7=pQ+8l$OjHrEGH~DM`+3C}aA2WxZ1A z%;bA2?>Te&Em)9!-`tGpa~DXqIn&d`jG2@%TbwanoRO8G#I2kiS^CJz!sRPREL!~F z?$s+thwa|E`d;4EvQFzf+@rx>Ivl3M2|Apn!}&Ta)!`Z)`gB;O!yP(&U5BT1ctMBF z8gy~3dPa;_LtvZ^OLVwFhkw@L-*vc0hX-}|jt;-l;ZHi`E3|$)>u`_`?_|&gSA(80 zONWbe_=pZS>F^Ia+^NF@I&9G4X&oN&heBTc%=3dLdS1Hp=Ahlu-F&FnxBb~iEt0XLfE{IBFxdl*3a;KxG5BR5|F|k#3`IX zxZ{EbDSklDqxkK?MFnjQKG6%C0H-6ojIaw~H3B&)b|7rkpG15qu#L&wG_uC=;_HioTtLIn-lfBXzbXGnPk*h$>%S@gPnML%=l+k^^gmfrHa_=%yQcril4|2~|EFvE zpDZakOzH1`TT@P1y1oyPO)D;0Q7GmwFDNeL&T(mFh53%c%%bAL9Q2{3rj{(6vGAe7 zMGhPFtL0^vl`JY;v4Z7flovVlH1{N*S+u->^~pWU;+aKdD;(5eMmcrKO(}Gwl`LC^ z0i~kl^XW~imbH9ENpWFTNkL(IC6+V8=_qwN(z5m3jO8nf%1V|mD_riFn_pIxzfeOI zoe~ylXx@eix5zL>d3Qsf4h0PMQru4%UBw2}1cv#=+4+to)AN@VPJTq2dk*JhWzORE zqBeA;{1uLjva*sgj(eKw07+*tqY-t0TF>0d)jDHjK~2KNR!~p|N-_LhS{9OGVScGT zrCE;rGKaI&zI<_sPK;TO0)0WaULDZQ?80TGgfBygTH1=)zdPb6o>^81lC9`K@tK8M zNf%?5v)EBI`4LCq?2??Kg2J>V`M{p*d#|&w>=7ENY+1gesAPHj(xOm#VMF)P95)N= z<18x!fzyl1u;?XakFar(|8I#u)1Ov&CRCth&Mqlh?kFtFD5vQ?BXs z3P)MVBb4PTS%nN*Hjq94LB?=;baDx+oLyMB^tM7SyJW@fmD`Z8F9##qX`67}4AUJm z3-b$Tz8tp=DTb1kdaAn+(m$lFH%3!a8talMSVO9_mqjxJCIe6Bm|X$~KmE z$TKwQ?aTN)T;RkV%2@%=1Dx=G;kz7|k=fBpE?#M;+)?W`#c?)kJ4DAgDj*#a2L~(tVEJR-o zT$J+k#vB+g%GVpu8M=OKJc1s z#m1n>BY2U~gaRL63j)o6;B>e-D1IGq!2k`3rGVs4BMK4R7K^%w6RaP|aonFY*ot^h z#0gdo;W#Ve1nUuKG6W~PaGIvXWWYBNQc>w!z#?4yP#g~jxt9=TBTn!cC^!t#6^awQW4Xp#1YLIgP{CZl<$9dpdOiLJz@2)WV6PGl z(Kx_*gq}c-;PYh~a}m6ZK--UB0V$0@{S%y^$5Q~OX>k{e6zT;8*XePBTlIKH2bKY$ z+n+V~ydEd`HUg29py<@%1fN9ci~0n+tkkHu3$PU-4*58gaf4PBfb>YY(uo1z5IKjV_gVdNY!JiRmmJB|^asL7mM*!EZh73SI1bqmzx9-k=c876y=Pg%E ze6C`u8acdaTlkBqFN!ZNd9n0G?j`Z1s+}+I+_kf2XYJ18I~#YN+1a#H-PydeWheI( z#EdehYEtZyZSHLww#8K^R3}xZR*Tiy)$^+Js%xu{S2tFlscx#?eX43S|8zps{|6af BI<5c! delta 20508 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�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 Result { // Create the virtualenv at the given location. let virtualenv = virtualenv::create( @@ -60,6 +65,8 @@ pub fn create_venv( allow_existing, relocatable, seed, + upgradeable, + preview, )?; // Create the corresponding `PythonEnvironment`. diff --git a/crates/uv-virtualenv/src/virtualenv.rs b/crates/uv-virtualenv/src/virtualenv.rs index a641e5541..bb6db01a3 100644 --- a/crates/uv-virtualenv/src/virtualenv.rs +++ b/crates/uv-virtualenv/src/virtualenv.rs @@ -10,8 +10,10 @@ use fs_err::File; use itertools::Itertools; use tracing::debug; +use uv_configuration::PreviewMode; use uv_fs::{CWD, Simplified, cachedir}; use uv_pypi_types::Scheme; +use uv_python::managed::{PythonMinorVersionLink, create_link_to_executable}; use uv_python::{Interpreter, VirtualEnvironment}; use uv_shell::escape_posix_for_single_quotes; use uv_version::version; @@ -53,6 +55,8 @@ pub(crate) fn create( allow_existing: bool, relocatable: bool, seed: bool, + upgradeable: bool, + preview: PreviewMode, ) -> Result { // Determine the base Python executable; that is, the Python executable that should be // considered the "base" for the virtual environment. @@ -143,13 +147,49 @@ pub(crate) fn create( // Create a `.gitignore` file to ignore all files in the venv. fs::write(location.join(".gitignore"), "*")?; + let executable_target = if upgradeable && interpreter.is_standalone() { + if let Some(minor_version_link) = PythonMinorVersionLink::from_executable( + base_python.as_path(), + &interpreter.key(), + preview, + ) { + if !minor_version_link.exists() { + base_python.clone() + } else { + let debug_symlink_term = if cfg!(windows) { + "junction" + } else { + "symlink directory" + }; + debug!( + "Using {} {} instead of base Python path: {}", + debug_symlink_term, + &minor_version_link.symlink_directory.display(), + &base_python.display() + ); + minor_version_link.symlink_executable.clone() + } + } else { + base_python.clone() + } + } else { + base_python.clone() + }; + // Per PEP 405, the Python `home` is the parent directory of the interpreter. - let python_home = base_python.parent().ok_or_else(|| { - io::Error::new( - io::ErrorKind::NotFound, - "The Python interpreter needs to have a parent directory", - ) - })?; + // In preview mode, for standalone interpreters, this `home` value will include a + // symlink directory on Unix or junction on Windows to enable transparent Python patch + // upgrades. + let python_home = executable_target + .parent() + .ok_or_else(|| { + io::Error::new( + io::ErrorKind::NotFound, + "The Python interpreter needs to have a parent directory", + ) + })? + .to_path_buf(); + let python_home = python_home.as_path(); // Different names for the python interpreter fs::create_dir_all(&scripts)?; @@ -157,7 +197,7 @@ pub(crate) fn create( #[cfg(unix)] { - uv_fs::replace_symlink(&base_python, &executable)?; + uv_fs::replace_symlink(&executable_target, &executable)?; uv_fs::replace_symlink( "python", scripts.join(format!("python{}", interpreter.python_major())), @@ -184,91 +224,102 @@ pub(crate) fn create( } } - // No symlinking on Windows, at least not on a regular non-dev non-admin Windows install. + // On Windows, we use trampolines that point to an executable target. For standalone + // interpreters, this target path includes a minor version junction to enable + // transparent upgrades. if cfg!(windows) { - copy_launcher_windows( - WindowsExecutable::Python, - interpreter, - &base_python, - &scripts, - python_home, - )?; - - if interpreter.markers().implementation_name() == "graalpy" { - copy_launcher_windows( - WindowsExecutable::GraalPy, - interpreter, - &base_python, - &scripts, - python_home, - )?; - copy_launcher_windows( - WindowsExecutable::PythonMajor, - interpreter, - &base_python, - &scripts, - python_home, - )?; + if interpreter.is_standalone() { + let target = scripts.join(WindowsExecutable::Python.exe(interpreter)); + create_link_to_executable(target.as_path(), executable_target.clone()) + .map_err(Error::Python)?; + let targetw = scripts.join(WindowsExecutable::Pythonw.exe(interpreter)); + create_link_to_executable(targetw.as_path(), executable_target) + .map_err(Error::Python)?; } else { copy_launcher_windows( - WindowsExecutable::Pythonw, + WindowsExecutable::Python, interpreter, &base_python, &scripts, python_home, )?; - } - if interpreter.markers().implementation_name() == "pypy" { - copy_launcher_windows( - WindowsExecutable::PythonMajor, - interpreter, - &base_python, - &scripts, - python_home, - )?; - copy_launcher_windows( - WindowsExecutable::PythonMajorMinor, - interpreter, - &base_python, - &scripts, - python_home, - )?; - copy_launcher_windows( - WindowsExecutable::PyPy, - interpreter, - &base_python, - &scripts, - python_home, - )?; - copy_launcher_windows( - WindowsExecutable::PyPyMajor, - interpreter, - &base_python, - &scripts, - python_home, - )?; - copy_launcher_windows( - WindowsExecutable::PyPyMajorMinor, - interpreter, - &base_python, - &scripts, - python_home, - )?; - copy_launcher_windows( - WindowsExecutable::PyPyw, - interpreter, - &base_python, - &scripts, - python_home, - )?; - copy_launcher_windows( - WindowsExecutable::PyPyMajorMinorw, - interpreter, - &base_python, - &scripts, - python_home, - )?; + if interpreter.markers().implementation_name() == "graalpy" { + copy_launcher_windows( + WindowsExecutable::GraalPy, + interpreter, + &base_python, + &scripts, + python_home, + )?; + copy_launcher_windows( + WindowsExecutable::PythonMajor, + interpreter, + &base_python, + &scripts, + python_home, + )?; + } else { + copy_launcher_windows( + WindowsExecutable::Pythonw, + interpreter, + &base_python, + &scripts, + python_home, + )?; + } + + if interpreter.markers().implementation_name() == "pypy" { + copy_launcher_windows( + WindowsExecutable::PythonMajor, + interpreter, + &base_python, + &scripts, + python_home, + )?; + copy_launcher_windows( + WindowsExecutable::PythonMajorMinor, + interpreter, + &base_python, + &scripts, + python_home, + )?; + copy_launcher_windows( + WindowsExecutable::PyPy, + interpreter, + &base_python, + &scripts, + python_home, + )?; + copy_launcher_windows( + WindowsExecutable::PyPyMajor, + interpreter, + &base_python, + &scripts, + python_home, + )?; + copy_launcher_windows( + WindowsExecutable::PyPyMajorMinor, + interpreter, + &base_python, + &scripts, + python_home, + )?; + copy_launcher_windows( + WindowsExecutable::PyPyw, + interpreter, + &base_python, + &scripts, + python_home, + )?; + copy_launcher_windows( + WindowsExecutable::PyPyMajorMinorw, + interpreter, + &base_python, + &scripts, + python_home, + )?; + } } } diff --git a/crates/uv/Cargo.toml b/crates/uv/Cargo.toml index b3accc211..de4fa3c38 100644 --- a/crates/uv/Cargo.toml +++ b/crates/uv/Cargo.toml @@ -70,10 +70,12 @@ clap = { workspace = true, features = ["derive", "string", "wrap_help"] } console = { workspace = true } ctrlc = { workspace = true } dotenvy = { workspace = true } +dunce = { workspace = true } flate2 = { workspace = true, default-features = false } fs-err = { workspace = true, features = ["tokio"] } futures = { workspace = true } http = { workspace = true } +indexmap = { workspace = true } indicatif = { workspace = true } indoc = { workspace = true } itertools = { workspace = true } diff --git a/crates/uv/src/commands/build_frontend.rs b/crates/uv/src/commands/build_frontend.rs index dd174ca06..9b97b40b1 100644 --- a/crates/uv/src/commands/build_frontend.rs +++ b/crates/uv/src/commands/build_frontend.rs @@ -499,6 +499,7 @@ async fn build_package( install_mirrors.python_install_mirror.as_deref(), install_mirrors.pypy_install_mirror.as_deref(), install_mirrors.python_downloads_json_url.as_deref(), + preview, ) .await? .into_interpreter(); diff --git a/crates/uv/src/commands/pip/check.rs b/crates/uv/src/commands/pip/check.rs index f504503af..bfbb20ee6 100644 --- a/crates/uv/src/commands/pip/check.rs +++ b/crates/uv/src/commands/pip/check.rs @@ -5,6 +5,7 @@ use anyhow::Result; use owo_colors::OwoColorize; use uv_cache::Cache; +use uv_configuration::PreviewMode; use uv_distribution_types::{Diagnostic, InstalledDist}; use uv_installer::{SitePackages, SitePackagesDiagnostic}; use uv_python::{EnvironmentPreference, PythonEnvironment, PythonRequest}; @@ -19,6 +20,7 @@ pub(crate) fn pip_check( system: bool, cache: &Cache, printer: Printer, + preview: PreviewMode, ) -> Result { let start = Instant::now(); @@ -27,6 +29,7 @@ pub(crate) fn pip_check( &python.map(PythonRequest::parse).unwrap_or_default(), EnvironmentPreference::from_system_flag(system, false), cache, + preview, )?; report_target_environment(&environment, cache, printer)?; diff --git a/crates/uv/src/commands/pip/compile.rs b/crates/uv/src/commands/pip/compile.rs index 197455aae..db80c2a8a 100644 --- a/crates/uv/src/commands/pip/compile.rs +++ b/crates/uv/src/commands/pip/compile.rs @@ -271,7 +271,13 @@ pub(crate) async fn pip_compile( let environment_preference = EnvironmentPreference::from_system_flag(system, false); let interpreter = if let Some(python) = python.as_ref() { let request = PythonRequest::parse(python); - PythonInstallation::find(&request, environment_preference, python_preference, &cache) + PythonInstallation::find( + &request, + environment_preference, + python_preference, + &cache, + preview, + ) } else { // TODO(zanieb): The split here hints at a problem with the request abstraction; we should // be able to use `PythonInstallation::find(...)` here. @@ -281,7 +287,13 @@ pub(crate) async fn pip_compile( } else { PythonRequest::default() }; - PythonInstallation::find_best(&request, environment_preference, python_preference, &cache) + PythonInstallation::find_best( + &request, + environment_preference, + python_preference, + &cache, + preview, + ) }? .into_interpreter(); diff --git a/crates/uv/src/commands/pip/freeze.rs b/crates/uv/src/commands/pip/freeze.rs index 7ad5517af..8c8491d45 100644 --- a/crates/uv/src/commands/pip/freeze.rs +++ b/crates/uv/src/commands/pip/freeze.rs @@ -6,6 +6,7 @@ use itertools::Itertools; use owo_colors::OwoColorize; use uv_cache::Cache; +use uv_configuration::PreviewMode; use uv_distribution_types::{Diagnostic, InstalledDist, Name}; use uv_installer::SitePackages; use uv_python::{EnvironmentPreference, PythonEnvironment, PythonRequest}; @@ -23,12 +24,14 @@ pub(crate) fn pip_freeze( paths: Option>, cache: &Cache, printer: Printer, + preview: PreviewMode, ) -> Result { // Detect the current Python interpreter. let environment = PythonEnvironment::find( &python.map(PythonRequest::parse).unwrap_or_default(), EnvironmentPreference::from_system_flag(system, false), cache, + preview, )?; report_target_environment(&environment, cache, printer)?; diff --git a/crates/uv/src/commands/pip/install.rs b/crates/uv/src/commands/pip/install.rs index e4f524c57..a92c36665 100644 --- a/crates/uv/src/commands/pip/install.rs +++ b/crates/uv/src/commands/pip/install.rs @@ -182,6 +182,7 @@ pub(crate) async fn pip_install( EnvironmentPreference::from_system_flag(system, false), python_preference, &cache, + preview, )?; report_interpreter(&installation, true, printer)?; PythonEnvironment::from_installation(installation) @@ -193,6 +194,7 @@ pub(crate) async fn pip_install( .unwrap_or_default(), EnvironmentPreference::from_system_flag(system, true), &cache, + preview, )?; report_target_environment(&environment, &cache, printer)?; environment diff --git a/crates/uv/src/commands/pip/list.rs b/crates/uv/src/commands/pip/list.rs index 824c9db2b..356574436 100644 --- a/crates/uv/src/commands/pip/list.rs +++ b/crates/uv/src/commands/pip/list.rs @@ -15,7 +15,7 @@ use uv_cache::{Cache, Refresh}; use uv_cache_info::Timestamp; use uv_cli::ListFormat; use uv_client::{BaseClientBuilder, RegistryClientBuilder}; -use uv_configuration::{Concurrency, IndexStrategy, KeyringProviderType}; +use uv_configuration::{Concurrency, IndexStrategy, KeyringProviderType, PreviewMode}; use uv_distribution_filename::DistFilename; use uv_distribution_types::{ Diagnostic, IndexCapabilities, IndexLocations, InstalledDist, Name, RequiresPython, @@ -54,6 +54,7 @@ pub(crate) async fn pip_list( system: bool, cache: &Cache, printer: Printer, + preview: PreviewMode, ) -> Result { // Disallow `--outdated` with `--format freeze`. if outdated && matches!(format, ListFormat::Freeze) { @@ -65,6 +66,7 @@ pub(crate) async fn pip_list( &python.map(PythonRequest::parse).unwrap_or_default(), EnvironmentPreference::from_system_flag(system, false), cache, + preview, )?; report_target_environment(&environment, cache, printer)?; diff --git a/crates/uv/src/commands/pip/show.rs b/crates/uv/src/commands/pip/show.rs index a77c29cd5..4d2b3c3a7 100644 --- a/crates/uv/src/commands/pip/show.rs +++ b/crates/uv/src/commands/pip/show.rs @@ -7,6 +7,7 @@ use owo_colors::OwoColorize; use rustc_hash::FxHashMap; use uv_cache::Cache; +use uv_configuration::PreviewMode; use uv_distribution_types::{Diagnostic, Name}; use uv_fs::Simplified; use uv_install_wheel::read_record_file; @@ -27,6 +28,7 @@ pub(crate) fn pip_show( files: bool, cache: &Cache, printer: Printer, + preview: PreviewMode, ) -> Result { if packages.is_empty() { #[allow(clippy::print_stderr)] @@ -46,6 +48,7 @@ pub(crate) fn pip_show( &python.map(PythonRequest::parse).unwrap_or_default(), EnvironmentPreference::from_system_flag(system, false), cache, + preview, )?; report_target_environment(&environment, cache, printer)?; diff --git a/crates/uv/src/commands/pip/sync.rs b/crates/uv/src/commands/pip/sync.rs index e5bf92ae4..e5145400a 100644 --- a/crates/uv/src/commands/pip/sync.rs +++ b/crates/uv/src/commands/pip/sync.rs @@ -157,6 +157,7 @@ pub(crate) async fn pip_sync( EnvironmentPreference::from_system_flag(system, false), python_preference, &cache, + preview, )?; report_interpreter(&installation, true, printer)?; PythonEnvironment::from_installation(installation) @@ -168,6 +169,7 @@ pub(crate) async fn pip_sync( .unwrap_or_default(), EnvironmentPreference::from_system_flag(system, true), &cache, + preview, )?; report_target_environment(&environment, &cache, printer)?; environment diff --git a/crates/uv/src/commands/pip/tree.rs b/crates/uv/src/commands/pip/tree.rs index aed364068..b0ba44c35 100644 --- a/crates/uv/src/commands/pip/tree.rs +++ b/crates/uv/src/commands/pip/tree.rs @@ -13,7 +13,7 @@ use tokio::sync::Semaphore; use uv_cache::{Cache, Refresh}; use uv_cache_info::Timestamp; use uv_client::{BaseClientBuilder, RegistryClientBuilder}; -use uv_configuration::{Concurrency, IndexStrategy, KeyringProviderType}; +use uv_configuration::{Concurrency, IndexStrategy, KeyringProviderType, PreviewMode}; use uv_distribution_types::{Diagnostic, IndexCapabilities, IndexLocations, Name, RequiresPython}; use uv_installer::SitePackages; use uv_normalize::PackageName; @@ -52,12 +52,14 @@ pub(crate) async fn pip_tree( system: bool, cache: &Cache, printer: Printer, + preview: PreviewMode, ) -> Result { // Detect the current Python interpreter. let environment = PythonEnvironment::find( &python.map(PythonRequest::parse).unwrap_or_default(), EnvironmentPreference::from_system_flag(system, false), cache, + preview, )?; report_target_environment(&environment, cache, printer)?; diff --git a/crates/uv/src/commands/pip/uninstall.rs b/crates/uv/src/commands/pip/uninstall.rs index 787ba5aae..4424fee37 100644 --- a/crates/uv/src/commands/pip/uninstall.rs +++ b/crates/uv/src/commands/pip/uninstall.rs @@ -7,7 +7,7 @@ use tracing::debug; use uv_cache::Cache; use uv_client::BaseClientBuilder; -use uv_configuration::{DryRun, KeyringProviderType}; +use uv_configuration::{DryRun, KeyringProviderType, PreviewMode}; use uv_distribution_types::Requirement; use uv_distribution_types::{InstalledMetadata, Name, UnresolvedRequirement}; use uv_fs::Simplified; @@ -37,6 +37,7 @@ pub(crate) async fn pip_uninstall( network_settings: &NetworkSettings, dry_run: DryRun, printer: Printer, + preview: PreviewMode, ) -> Result { let start = std::time::Instant::now(); @@ -57,6 +58,7 @@ pub(crate) async fn pip_uninstall( .unwrap_or_default(), EnvironmentPreference::from_system_flag(system, true), &cache, + preview, )?; report_target_environment(&environment, &cache, printer)?; diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index 1c5297f90..ae20b31d4 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -195,6 +195,7 @@ pub(crate) async fn add( &client_builder, cache, &reporter, + preview, ) .await?; Pep723Script::init(&path, requires_python.specifiers()).await? @@ -217,6 +218,7 @@ pub(crate) async fn add( active, cache, printer, + preview, ) .await? .into_interpreter(); @@ -286,6 +288,7 @@ pub(crate) async fn add( active, cache, printer, + preview, ) .await? .into_interpreter(); @@ -307,6 +310,7 @@ pub(crate) async fn add( cache, DryRun::Disabled, printer, + preview, ) .await? .into_environment()?; diff --git a/crates/uv/src/commands/project/environment.rs b/crates/uv/src/commands/project/environment.rs index f7ba006c5..da3dc7f63 100644 --- a/crates/uv/src/commands/project/environment.rs +++ b/crates/uv/src/commands/project/environment.rs @@ -7,7 +7,7 @@ use uv_cache_key::{cache_digest, hash_digest}; use uv_configuration::{Concurrency, Constraints, PreviewMode}; use uv_distribution_types::{Name, Resolution}; use uv_fs::PythonExt; -use uv_python::{Interpreter, PythonEnvironment}; +use uv_python::{Interpreter, PythonEnvironment, canonicalize_executable}; use crate::commands::pip::loggers::{InstallLogger, ResolveLogger}; use crate::commands::pip::operations::Modifications; @@ -74,7 +74,8 @@ impl CachedEnvironment { // Hash the interpreter based on its path. // TODO(charlie): Come up with a robust hash for the interpreter. - let interpreter_hash = cache_digest(&interpreter.sys_executable()); + let interpreter_hash = + cache_digest(&canonicalize_executable(interpreter.sys_executable())?); // Search in the content-addressed cache. let cache_entry = cache.entry(CacheBucket::Environments, interpreter_hash, resolution_hash); @@ -97,6 +98,8 @@ impl CachedEnvironment { false, true, false, + false, + preview, )?; sync_environment( diff --git a/crates/uv/src/commands/project/export.rs b/crates/uv/src/commands/project/export.rs index ac228989c..88a847d04 100644 --- a/crates/uv/src/commands/project/export.rs +++ b/crates/uv/src/commands/project/export.rs @@ -142,6 +142,7 @@ pub(crate) async fn export( Some(false), cache, printer, + preview, ) .await? .into_interpreter(), @@ -159,6 +160,7 @@ pub(crate) async fn export( Some(false), cache, printer, + preview, ) .await? .into_interpreter(), diff --git a/crates/uv/src/commands/project/init.rs b/crates/uv/src/commands/project/init.rs index 71aacdc1b..15fed409e 100644 --- a/crates/uv/src/commands/project/init.rs +++ b/crates/uv/src/commands/project/init.rs @@ -87,6 +87,7 @@ pub(crate) async fn init( pin_python, package, no_config, + preview, ) .await?; @@ -202,6 +203,7 @@ async fn init_script( pin_python: bool, package: bool, no_config: bool, + preview: PreviewMode, ) -> Result<()> { if no_workspace { warn_user_once!("`--no-workspace` is a no-op for Python scripts, which are standalone"); @@ -258,6 +260,7 @@ async fn init_script( &client_builder, cache, &reporter, + preview, ) .await?; @@ -434,6 +437,7 @@ async fn init_project( install_mirrors.python_install_mirror.as_deref(), install_mirrors.pypy_install_mirror.as_deref(), install_mirrors.python_downloads_json_url.as_deref(), + preview, ) .await? .into_interpreter(); @@ -461,6 +465,7 @@ async fn init_project( install_mirrors.python_install_mirror.as_deref(), install_mirrors.pypy_install_mirror.as_deref(), install_mirrors.python_downloads_json_url.as_deref(), + preview, ) .await? .into_interpreter(); @@ -527,6 +532,7 @@ async fn init_project( install_mirrors.python_install_mirror.as_deref(), install_mirrors.pypy_install_mirror.as_deref(), install_mirrors.python_downloads_json_url.as_deref(), + preview, ) .await? .into_interpreter(); @@ -554,6 +560,7 @@ async fn init_project( install_mirrors.python_install_mirror.as_deref(), install_mirrors.pypy_install_mirror.as_deref(), install_mirrors.python_downloads_json_url.as_deref(), + preview, ) .await? .into_interpreter(); diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index 1ab4441b0..97ee01767 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -114,6 +114,7 @@ pub(crate) async fn lock( &client_builder, cache, &reporter, + preview, ) .await?; Some(Pep723Script::init(&path, requires_python.specifiers()).await?) @@ -155,6 +156,7 @@ pub(crate) async fn lock( Some(false), cache, printer, + preview, ) .await? .into_interpreter(), @@ -170,6 +172,7 @@ pub(crate) async fn lock( Some(false), cache, printer, + preview, ) .await? .into_interpreter(), diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index 85defd4dd..a3249b11a 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -625,6 +625,7 @@ impl ScriptInterpreter { active: Option, cache: &Cache, printer: Printer, + preview: PreviewMode, ) -> Result { // For now, we assume that scripts are never evaluated in the context of a workspace. let workspace = None; @@ -682,6 +683,7 @@ impl ScriptInterpreter { install_mirrors.python_install_mirror.as_deref(), install_mirrors.pypy_install_mirror.as_deref(), install_mirrors.python_downloads_json_url.as_deref(), + preview, ) .await? .into_interpreter(); @@ -841,6 +843,7 @@ impl ProjectInterpreter { active: Option, cache: &Cache, printer: Printer, + preview: PreviewMode, ) -> Result { // Resolve the Python request and requirement for the workspace. let WorkspacePython { @@ -937,6 +940,7 @@ impl ProjectInterpreter { install_mirrors.python_install_mirror.as_deref(), install_mirrors.pypy_install_mirror.as_deref(), install_mirrors.python_downloads_json_url.as_deref(), + preview, ) .await?; @@ -1213,10 +1217,16 @@ impl ProjectEnvironment { cache: &Cache, dry_run: DryRun, printer: Printer, + preview: PreviewMode, ) -> Result { // Lock the project environment to avoid synchronization issues. let _lock = ProjectInterpreter::lock(workspace).await?; + let upgradeable = preview.is_enabled() + && python + .as_ref() + .is_none_or(|request| !request.includes_patch()); + match ProjectInterpreter::discover( workspace, workspace.install_path().as_ref(), @@ -1231,6 +1241,7 @@ impl ProjectEnvironment { active, cache, printer, + preview, ) .await? { @@ -1300,6 +1311,8 @@ impl ProjectEnvironment { false, false, false, + upgradeable, + preview, )?; return Ok(if replace { Self::WouldReplace(root, environment, temp_dir) @@ -1337,6 +1350,8 @@ impl ProjectEnvironment { false, false, false, + upgradeable, + preview, )?; if replace { @@ -1420,9 +1435,13 @@ impl ScriptEnvironment { cache: &Cache, dry_run: DryRun, printer: Printer, + preview: PreviewMode, ) -> Result { // Lock the script environment to avoid synchronization issues. let _lock = ScriptInterpreter::lock(script).await?; + let upgradeable = python_request + .as_ref() + .is_none_or(|request| !request.includes_patch()); match ScriptInterpreter::discover( script, @@ -1436,6 +1455,7 @@ impl ScriptEnvironment { active, cache, printer, + preview, ) .await? { @@ -1468,6 +1488,8 @@ impl ScriptEnvironment { false, false, false, + upgradeable, + preview, )?; return Ok(if root.exists() { Self::WouldReplace(root, environment, temp_dir) @@ -1502,6 +1524,8 @@ impl ScriptEnvironment { false, false, false, + upgradeable, + preview, )?; Ok(if replaced { @@ -2333,6 +2357,7 @@ pub(crate) async fn init_script_python_requirement( client_builder: &BaseClientBuilder<'_>, cache: &Cache, reporter: &PythonDownloadReporter, + preview: PreviewMode, ) -> anyhow::Result { let python_request = if let Some(request) = python { // (1) Explicit request from user @@ -2364,6 +2389,7 @@ pub(crate) async fn init_script_python_requirement( install_mirrors.python_install_mirror.as_deref(), install_mirrors.pypy_install_mirror.as_deref(), install_mirrors.python_downloads_json_url.as_deref(), + preview, ) .await? .into_interpreter(); diff --git a/crates/uv/src/commands/project/remove.rs b/crates/uv/src/commands/project/remove.rs index d17cd88ed..7fd02277e 100644 --- a/crates/uv/src/commands/project/remove.rs +++ b/crates/uv/src/commands/project/remove.rs @@ -229,6 +229,7 @@ pub(crate) async fn remove( active, cache, printer, + preview, ) .await? .into_interpreter(); @@ -250,6 +251,7 @@ pub(crate) async fn remove( cache, DryRun::Disabled, printer, + preview, ) .await? .into_environment()?; @@ -270,6 +272,7 @@ pub(crate) async fn remove( active, cache, printer, + preview, ) .await? .into_interpreter(); diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index f97ffdbc1..8a510fa8c 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -235,6 +235,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl cache, DryRun::Disabled, printer, + preview, ) .await? .into_environment()?; @@ -359,6 +360,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl cache, DryRun::Disabled, printer, + preview, ) .await? .into_environment()?; @@ -433,6 +435,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl active.map_or(Some(false), Some), cache, printer, + preview, ) .await? .into_interpreter(); @@ -446,6 +449,8 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl false, false, false, + false, + preview, )?; Some(environment.into_interpreter()) @@ -624,6 +629,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl install_mirrors.python_install_mirror.as_deref(), install_mirrors.pypy_install_mirror.as_deref(), install_mirrors.python_downloads_json_url.as_deref(), + preview, ) .await? .into_interpreter(); @@ -648,6 +654,8 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl false, false, false, + false, + preview, )? } else { // If we're not isolating the environment, reuse the base environment for the @@ -666,6 +674,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl cache, DryRun::Disabled, printer, + preview, ) .await? .into_environment()? @@ -850,6 +859,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl install_mirrors.python_install_mirror.as_deref(), install_mirrors.pypy_install_mirror.as_deref(), install_mirrors.python_downloads_json_url.as_deref(), + preview, ) .await?; @@ -869,6 +879,8 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl false, false, false, + false, + preview, )?; venv.into_interpreter() } else { diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index 940b3a653..f1a73b8c8 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -145,6 +145,7 @@ pub(crate) async fn sync( cache, dry_run, printer, + preview, ) .await?, ), @@ -162,6 +163,7 @@ pub(crate) async fn sync( cache, dry_run, printer, + preview, ) .await?, ), diff --git a/crates/uv/src/commands/project/tree.rs b/crates/uv/src/commands/project/tree.rs index 9c42a8a86..2ff6ad98e 100644 --- a/crates/uv/src/commands/project/tree.rs +++ b/crates/uv/src/commands/project/tree.rs @@ -97,6 +97,7 @@ pub(crate) async fn tree( Some(false), cache, printer, + preview, ) .await? .into_interpreter(), @@ -114,6 +115,7 @@ pub(crate) async fn tree( Some(false), cache, printer, + preview, ) .await? .into_interpreter(), diff --git a/crates/uv/src/commands/project/version.rs b/crates/uv/src/commands/project/version.rs index f76744186..fdba41978 100644 --- a/crates/uv/src/commands/project/version.rs +++ b/crates/uv/src/commands/project/version.rs @@ -296,6 +296,7 @@ async fn print_frozen_version( active, cache, printer, + preview, ) .await? .into_interpreter(); @@ -403,6 +404,7 @@ async fn lock_and_sync( active, cache, printer, + preview, ) .await? .into_interpreter(); @@ -424,6 +426,7 @@ async fn lock_and_sync( cache, DryRun::Disabled, printer, + preview, ) .await? .into_environment()?; diff --git a/crates/uv/src/commands/python/find.rs b/crates/uv/src/commands/python/find.rs index 1e5693c65..e188e9d20 100644 --- a/crates/uv/src/commands/python/find.rs +++ b/crates/uv/src/commands/python/find.rs @@ -1,9 +1,9 @@ use anyhow::Result; use std::fmt::Write; use std::path::Path; -use uv_configuration::DependencyGroupsWithDefaults; use uv_cache::Cache; +use uv_configuration::{DependencyGroupsWithDefaults, PreviewMode}; use uv_fs::Simplified; use uv_python::{ EnvironmentPreference, PythonDownloads, PythonInstallation, PythonPreference, PythonRequest, @@ -32,6 +32,7 @@ pub(crate) async fn find( python_preference: PythonPreference, cache: &Cache, printer: Printer, + preview: PreviewMode, ) -> Result { let environment_preference = if system { EnvironmentPreference::OnlySystem @@ -77,6 +78,7 @@ pub(crate) async fn find( environment_preference, python_preference, cache, + preview, )?; // Warn if the discovered Python version is incompatible with the current workspace @@ -121,6 +123,7 @@ pub(crate) async fn find_script( no_config: bool, cache: &Cache, printer: Printer, + preview: PreviewMode, ) -> Result { let interpreter = match ScriptInterpreter::discover( script, @@ -134,6 +137,7 @@ pub(crate) async fn find_script( Some(false), cache, printer, + preview, ) .await { diff --git a/crates/uv/src/commands/python/install.rs b/crates/uv/src/commands/python/install.rs index 8f5beedc9..7ad96fffe 100644 --- a/crates/uv/src/commands/python/install.rs +++ b/crates/uv/src/commands/python/install.rs @@ -2,10 +2,12 @@ use std::borrow::Cow; use std::fmt::Write; use std::io::ErrorKind; use std::path::{Path, PathBuf}; +use std::str::FromStr; use anyhow::{Error, Result}; use futures::StreamExt; use futures::stream::FuturesUnordered; +use indexmap::IndexSet; use itertools::{Either, Itertools}; use owo_colors::OwoColorize; use rustc_hash::{FxHashMap, FxHashSet}; @@ -15,12 +17,13 @@ use uv_configuration::PreviewMode; use uv_fs::Simplified; use uv_python::downloads::{self, DownloadResult, ManagedPythonDownload, PythonDownloadRequest}; use uv_python::managed::{ - ManagedPythonInstallation, ManagedPythonInstallations, python_executable_dir, + ManagedPythonInstallation, ManagedPythonInstallations, PythonMinorVersionLink, + create_link_to_executable, python_executable_dir, }; use uv_python::platform::{Arch, Libc}; use uv_python::{ - PythonDownloads, PythonInstallationKey, PythonRequest, PythonVersionFile, - VersionFileDiscoveryOptions, VersionFilePreference, + PythonDownloads, PythonInstallationKey, PythonInstallationMinorVersionKey, PythonRequest, + PythonVersionFile, VersionFileDiscoveryOptions, VersionFilePreference, VersionRequest, }; use uv_shell::Shell; use uv_trampoline_builder::{Launcher, LauncherKind}; @@ -32,7 +35,7 @@ use crate::commands::{ExitStatus, elapsed}; use crate::printer::Printer; use crate::settings::NetworkSettings; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] struct InstallRequest { /// The original request from the user request: PythonRequest, @@ -82,6 +85,10 @@ impl InstallRequest { fn matches_installation(&self, installation: &ManagedPythonInstallation) -> bool { self.download_request.satisfied_by_key(installation.key()) } + + fn python_request(&self) -> &PythonRequest { + &self.request + } } impl std::fmt::Display for InstallRequest { @@ -132,6 +139,7 @@ pub(crate) async fn install( install_dir: Option, targets: Vec, reinstall: bool, + upgrade: bool, force: bool, python_install_mirror: Option, pypy_install_mirror: Option, @@ -153,34 +161,66 @@ pub(crate) async fn install( return Ok(ExitStatus::Failure); } + if upgrade && preview.is_disabled() { + warn_user!( + "`uv python upgrade` is experimental and may change without warning. Pass `--preview` to disable this warning" + ); + } + if default && targets.len() > 1 { anyhow::bail!("The `--default` flag cannot be used with multiple targets"); } + // Read the existing installations, lock the directory for the duration + let installations = ManagedPythonInstallations::from_settings(install_dir.clone())?.init()?; + let installations_dir = installations.root(); + let scratch_dir = installations.scratch(); + let _lock = installations.lock().await?; + let existing_installations: Vec<_> = installations + .find_all()? + .inspect(|installation| trace!("Found existing installation {}", installation.key())) + .collect(); + // Resolve the requests let mut is_default_install = false; + let mut is_unspecified_upgrade = false; let requests: Vec<_> = if targets.is_empty() { - PythonVersionFile::discover( - project_dir, - &VersionFileDiscoveryOptions::default() - .with_no_config(no_config) - .with_preference(VersionFilePreference::Versions), - ) - .await? - .map(PythonVersionFile::into_versions) - .unwrap_or_else(|| { - // If no version file is found and no requests were made - is_default_install = true; - vec![if reinstall { - // On bare `--reinstall`, reinstall all Python versions - PythonRequest::Any - } else { - PythonRequest::Default - }] - }) - .into_iter() - .map(|a| InstallRequest::new(a, python_downloads_json_url.as_deref())) - .collect::>>()? + if upgrade { + is_unspecified_upgrade = true; + let mut minor_version_requests = IndexSet::::default(); + for installation in &existing_installations { + let request = VersionRequest::major_minor_request_from_key(installation.key()); + if let Ok(request) = InstallRequest::new( + PythonRequest::Version(request), + python_downloads_json_url.as_deref(), + ) { + minor_version_requests.insert(request); + } + } + minor_version_requests.into_iter().collect::>() + } else { + PythonVersionFile::discover( + project_dir, + &VersionFileDiscoveryOptions::default() + .with_no_config(no_config) + .with_preference(VersionFilePreference::Versions), + ) + .await? + .map(PythonVersionFile::into_versions) + .unwrap_or_else(|| { + // If no version file is found and no requests were made + is_default_install = true; + vec![if reinstall { + // On bare `--reinstall`, reinstall all Python versions + PythonRequest::Any + } else { + PythonRequest::Default + }] + }) + .into_iter() + .map(|a| InstallRequest::new(a, python_downloads_json_url.as_deref())) + .collect::>>()? + } } else { targets .iter() @@ -190,18 +230,39 @@ pub(crate) async fn install( }; let Some(first_request) = requests.first() else { + if upgrade { + writeln!( + printer.stderr(), + "There are no installed versions to upgrade" + )?; + } return Ok(ExitStatus::Success); }; - // Read the existing installations, lock the directory for the duration - let installations = ManagedPythonInstallations::from_settings(install_dir)?.init()?; - let installations_dir = installations.root(); - let scratch_dir = installations.scratch(); - let _lock = installations.lock().await?; - let existing_installations: Vec<_> = installations - .find_all()? - .inspect(|installation| trace!("Found existing installation {}", installation.key())) - .collect(); + let requested_minor_versions = requests + .iter() + .filter_map(|request| { + if let PythonRequest::Version(VersionRequest::MajorMinor(major, minor, ..)) = + request.python_request() + { + uv_pep440::Version::from_str(&format!("{major}.{minor}")).ok() + } else { + None + } + }) + .collect::>(); + + if upgrade + && requests + .iter() + .any(|request| request.request.includes_patch()) + { + writeln!( + printer.stderr(), + "error: `uv python upgrade` only accepts minor versions" + )?; + return Ok(ExitStatus::Failure); + } // Find requests that are already satisfied let mut changelog = Changelog::default(); @@ -259,15 +320,20 @@ pub(crate) async fn install( } } } - (vec![], unsatisfied) } else { // If we can find one existing installation that matches the request, it is satisfied requests.iter().partition_map(|request| { - if let Some(installation) = existing_installations - .iter() - .find(|installation| request.matches_installation(installation)) - { + if let Some(installation) = existing_installations.iter().find(|installation| { + if upgrade { + // If this is an upgrade, the requested version is a minor version + // but the requested download is the highest patch for that minor + // version. We need to install it unless an exact match is found. + request.download.key() == installation.key() + } else { + request.matches_installation(installation) + } + }) { debug!( "Found `{}` for request `{}`", installation.key().green(), @@ -385,18 +451,24 @@ pub(crate) async fn install( .expect("We should have a bin directory with preview enabled") .as_path(); + let upgradeable = preview.is_enabled() && is_default_install + || requested_minor_versions.contains(&installation.key().version().python_version()); + create_bin_links( installation, bin, reinstall, force, default, + upgradeable, + upgrade, is_default_install, first_request, &existing_installations, &installations, &mut changelog, &mut errors, + preview, )?; if preview.is_enabled() { @@ -407,14 +479,51 @@ pub(crate) async fn install( } } + let minor_versions = + PythonInstallationMinorVersionKey::highest_installations_by_minor_version_key( + installations + .iter() + .copied() + .chain(existing_installations.iter()), + ); + + for installation in minor_versions.values() { + if upgrade { + // During an upgrade, update existing symlinks but avoid + // creating new ones. + installation.update_minor_version_link(preview)?; + } else { + installation.ensure_minor_version_link(preview)?; + } + } + if changelog.installed.is_empty() && errors.is_empty() { if is_default_install { writeln!( printer.stderr(), "Python is already installed. Use `uv python install ` to install another version.", )?; + } else if upgrade && requests.is_empty() { + writeln!( + printer.stderr(), + "There are no installed versions to upgrade" + )?; } else if requests.len() > 1 { - writeln!(printer.stderr(), "All requested versions already installed")?; + if upgrade { + if is_unspecified_upgrade { + writeln!( + printer.stderr(), + "All versions already on latest supported patch release" + )?; + } else { + writeln!( + printer.stderr(), + "All requested versions already on latest supported patch release" + )?; + } + } else { + writeln!(printer.stderr(), "All requested versions already installed")?; + } } return Ok(ExitStatus::Success); } @@ -520,12 +629,15 @@ fn create_bin_links( reinstall: bool, force: bool, default: bool, + upgradeable: bool, + upgrade: bool, is_default_install: bool, first_request: &InstallRequest, existing_installations: &[ManagedPythonInstallation], installations: &[&ManagedPythonInstallation], changelog: &mut Changelog, errors: &mut Vec<(PythonInstallationKey, Error)>, + preview: PreviewMode, ) -> Result<(), Error> { let targets = if (default || is_default_install) && first_request.matches_installation(installation) { @@ -540,7 +652,19 @@ fn create_bin_links( for target in targets { let target = bin.join(target); - match installation.create_bin_link(&target) { + let executable = if upgradeable { + if let Some(minor_version_link) = + PythonMinorVersionLink::from_installation(installation, preview) + { + minor_version_link.symlink_executable.clone() + } else { + installation.executable(false) + } + } else { + installation.executable(false) + }; + + match create_link_to_executable(&target, executable.clone()) { Ok(()) => { debug!( "Installed executable at `{}` for {}", @@ -589,13 +713,23 @@ fn create_bin_links( // There's an existing executable we don't manage, require `--force` if valid_link { if !force { - errors.push(( - installation.key().clone(), - anyhow::anyhow!( - "Executable already exists at `{}` but is not managed by uv; use `--force` to replace it", - to.simplified_display() - ), - )); + if upgrade { + warn_user!( + "Executable already exists at `{}` but is not managed by uv; use `uv python install {}.{}{} --force` to replace it", + to.simplified_display(), + installation.key().major(), + installation.key().minor(), + installation.key().variant().suffix() + ); + } else { + errors.push(( + installation.key().clone(), + anyhow::anyhow!( + "Executable already exists at `{}` but is not managed by uv; use `--force` to replace it", + to.simplified_display() + ), + )); + } continue; } debug!( @@ -676,7 +810,7 @@ fn create_bin_links( .remove(&target); } - installation.create_bin_link(&target)?; + create_link_to_executable(&target, executable)?; debug!( "Updated executable at `{}` to {}", target.simplified_display(), @@ -747,8 +881,7 @@ fn warn_if_not_on_path(bin: &Path) { /// Find the [`ManagedPythonInstallation`] corresponding to an executable link installed at the /// given path, if any. /// -/// Like [`ManagedPythonInstallation::is_bin_link`], but this method will only resolve the -/// given path one time. +/// Will resolve symlinks on Unix. On Windows, will resolve the target link for a trampoline. fn find_matching_bin_link<'a>( mut installations: impl Iterator, path: &Path, @@ -757,13 +890,13 @@ fn find_matching_bin_link<'a>( if !path.is_symlink() { return None; } - path.read_link().ok()? + fs_err::canonicalize(path).ok()? } else if cfg!(windows) { let launcher = Launcher::try_from_path(path).ok()??; if !matches!(launcher.kind, LauncherKind::Python) { return None; } - launcher.python_path + dunce::canonicalize(launcher.python_path).ok()? } else { unreachable!("Only Windows and Unix are supported") }; diff --git a/crates/uv/src/commands/python/list.rs b/crates/uv/src/commands/python/list.rs index 71bfb9c55..2cd54747c 100644 --- a/crates/uv/src/commands/python/list.rs +++ b/crates/uv/src/commands/python/list.rs @@ -2,6 +2,7 @@ use serde::Serialize; use std::collections::BTreeSet; use std::fmt::Write; use uv_cli::PythonListFormat; +use uv_configuration::PreviewMode; use uv_pep440::Version; use anyhow::Result; @@ -64,6 +65,7 @@ pub(crate) async fn list( python_downloads: PythonDownloads, cache: &Cache, printer: Printer, + preview: PreviewMode, ) -> Result { let request = request.as_deref().map(PythonRequest::parse); let base_download_request = if python_preference == PythonPreference::OnlySystem { @@ -124,6 +126,7 @@ pub(crate) async fn list( EnvironmentPreference::OnlySystem, python_preference, cache, + preview, ) // Raise discovery errors if critical .filter(|result| { diff --git a/crates/uv/src/commands/python/pin.rs b/crates/uv/src/commands/python/pin.rs index a0af7ec41..26714e4d7 100644 --- a/crates/uv/src/commands/python/pin.rs +++ b/crates/uv/src/commands/python/pin.rs @@ -8,7 +8,7 @@ use tracing::debug; use uv_cache::Cache; use uv_client::BaseClientBuilder; -use uv_configuration::DependencyGroupsWithDefaults; +use uv_configuration::{DependencyGroupsWithDefaults, PreviewMode}; use uv_dirs::user_uv_config_dir; use uv_fs::Simplified; use uv_python::{ @@ -40,6 +40,7 @@ pub(crate) async fn pin( network_settings: NetworkSettings, cache: &Cache, printer: Printer, + preview: PreviewMode, ) -> Result { let workspace_cache = WorkspaceCache::default(); let virtual_project = if no_project { @@ -94,6 +95,7 @@ pub(crate) async fn pin( virtual_project, python_preference, cache, + preview, ); } } @@ -124,6 +126,7 @@ pub(crate) async fn pin( install_mirrors.python_install_mirror.as_deref(), install_mirrors.pypy_install_mirror.as_deref(), install_mirrors.python_downloads_json_url.as_deref(), + preview, ) .await { @@ -260,6 +263,7 @@ fn warn_if_existing_pin_incompatible_with_project( virtual_project: &VirtualProject, python_preference: PythonPreference, cache: &Cache, + preview: PreviewMode, ) { // Check if the pinned version is compatible with the project. if let Some(pin_version) = pep440_version_from_request(pin) { @@ -284,6 +288,7 @@ fn warn_if_existing_pin_incompatible_with_project( EnvironmentPreference::OnlySystem, python_preference, cache, + preview, ) { Ok(python) => { let python_version = python.python_version(); diff --git a/crates/uv/src/commands/python/uninstall.rs b/crates/uv/src/commands/python/uninstall.rs index ac159344c..8a63b015c 100644 --- a/crates/uv/src/commands/python/uninstall.rs +++ b/crates/uv/src/commands/python/uninstall.rs @@ -5,6 +5,7 @@ use std::path::PathBuf; use anyhow::Result; use futures::StreamExt; use futures::stream::FuturesUnordered; +use indexmap::IndexSet; use itertools::Itertools; use owo_colors::OwoColorize; use rustc_hash::{FxHashMap, FxHashSet}; @@ -13,8 +14,10 @@ use tracing::{debug, warn}; use uv_configuration::PreviewMode; use uv_fs::Simplified; use uv_python::downloads::PythonDownloadRequest; -use uv_python::managed::{ManagedPythonInstallations, python_executable_dir}; -use uv_python::{PythonInstallationKey, PythonRequest}; +use uv_python::managed::{ + ManagedPythonInstallations, PythonMinorVersionLink, python_executable_dir, +}; +use uv_python::{PythonInstallationKey, PythonInstallationMinorVersionKey, PythonRequest}; use crate::commands::python::install::format_executables; use crate::commands::python::{ChangeEvent, ChangeEventKind}; @@ -87,7 +90,6 @@ async fn do_uninstall( // Always include pre-releases in uninstalls .map(|result| result.map(|request| request.with_prereleases(true))) .collect::>>()?; - let installed_installations: Vec<_> = installations.find_all()?.collect(); let mut matching_installations = BTreeSet::default(); for (request, download_request) in requests.iter().zip(download_requests) { @@ -218,6 +220,63 @@ async fn do_uninstall( uv_python::windows_registry::remove_orphan_registry_entries(&installed_installations); } + // Read all existing managed installations and find the highest installed patch + // for each installed minor version. Ensure the minor version link directory + // is still valid. + let uninstalled_minor_versions = &uninstalled.iter().fold( + IndexSet::<&PythonInstallationMinorVersionKey>::default(), + |mut minor_versions, key| { + minor_versions.insert(PythonInstallationMinorVersionKey::ref_cast(key)); + minor_versions + }, + ); + let remaining_installations: Vec<_> = installations.find_all()?.collect(); + let remaining_minor_versions = + PythonInstallationMinorVersionKey::highest_installations_by_minor_version_key( + remaining_installations.iter(), + ); + + for (_, installation) in remaining_minor_versions + .iter() + .filter(|(minor_version, _)| uninstalled_minor_versions.contains(minor_version)) + { + installation.ensure_minor_version_link(preview)?; + } + // For each uninstalled installation, check if there are no remaining installations + // for its minor version. If there are none remaining, remove the symlink directory + // (or junction on Windows) if it exists. + for installation in &matching_installations { + if !remaining_minor_versions.contains_key(installation.minor_version_key()) { + if let Some(minor_version_link) = + PythonMinorVersionLink::from_installation(installation, preview) + { + if minor_version_link.exists() { + let result = if cfg!(windows) { + fs_err::remove_dir(minor_version_link.symlink_directory.as_path()) + } else { + fs_err::remove_file(minor_version_link.symlink_directory.as_path()) + }; + if result.is_err() { + return Err(anyhow::anyhow!( + "Failed to remove symlink directory {}", + minor_version_link.symlink_directory.display() + )); + } + let symlink_term = if cfg!(windows) { + "junction" + } else { + "symlink directory" + }; + debug!( + "Removed {}: {}", + symlink_term, + minor_version_link.symlink_directory.to_string_lossy() + ); + } + } + } + } + // Report on any uninstalled installations. if !uninstalled.is_empty() { if let [uninstalled] = uninstalled.as_slice() { diff --git a/crates/uv/src/commands/tool/common.rs b/crates/uv/src/commands/tool/common.rs index 807225cbc..166b4fc6f 100644 --- a/crates/uv/src/commands/tool/common.rs +++ b/crates/uv/src/commands/tool/common.rs @@ -7,6 +7,7 @@ use std::{collections::BTreeSet, ffi::OsString}; use tracing::{debug, warn}; use uv_cache::Cache; use uv_client::BaseClientBuilder; +use uv_configuration::PreviewMode; use uv_distribution_types::Requirement; use uv_distribution_types::{InstalledDist, Name}; use uv_fs::Simplified; @@ -80,6 +81,7 @@ pub(crate) async fn refine_interpreter( python_preference: PythonPreference, python_downloads: PythonDownloads, cache: &Cache, + preview: PreviewMode, ) -> anyhow::Result, ProjectError> { let pip::operations::Error::Resolve(uv_resolver::ResolveError::NoSolution(no_solution_err)) = err @@ -151,6 +153,7 @@ pub(crate) async fn refine_interpreter( install_mirrors.python_install_mirror.as_deref(), install_mirrors.pypy_install_mirror.as_deref(), install_mirrors.python_downloads_json_url.as_deref(), + preview, ) .await? .into_interpreter(); diff --git a/crates/uv/src/commands/tool/install.rs b/crates/uv/src/commands/tool/install.rs index a65ad3af2..e816e771e 100644 --- a/crates/uv/src/commands/tool/install.rs +++ b/crates/uv/src/commands/tool/install.rs @@ -87,6 +87,7 @@ pub(crate) async fn install( install_mirrors.python_install_mirror.as_deref(), install_mirrors.pypy_install_mirror.as_deref(), install_mirrors.python_downloads_json_url.as_deref(), + preview, ) .await? .into_interpreter(); @@ -508,6 +509,7 @@ pub(crate) async fn install( python_preference, python_downloads, &cache, + preview, ) .await .ok() @@ -554,7 +556,7 @@ pub(crate) async fn install( }, }; - let environment = installed_tools.create_environment(&from.name, interpreter)?; + let environment = installed_tools.create_environment(&from.name, interpreter, preview)?; // At this point, we removed any existing environment, so we should remove any of its // executables. diff --git a/crates/uv/src/commands/tool/run.rs b/crates/uv/src/commands/tool/run.rs index 4d270c445..2746d65ad 100644 --- a/crates/uv/src/commands/tool/run.rs +++ b/crates/uv/src/commands/tool/run.rs @@ -747,6 +747,7 @@ async fn get_or_create_environment( install_mirrors.python_install_mirror.as_deref(), install_mirrors.pypy_install_mirror.as_deref(), install_mirrors.python_downloads_json_url.as_deref(), + preview, ) .await? .into_interpreter(); @@ -1036,6 +1037,7 @@ async fn get_or_create_environment( python_preference, python_downloads, cache, + preview, ) .await .ok() diff --git a/crates/uv/src/commands/tool/upgrade.rs b/crates/uv/src/commands/tool/upgrade.rs index c930ecada..166e00349 100644 --- a/crates/uv/src/commands/tool/upgrade.rs +++ b/crates/uv/src/commands/tool/upgrade.rs @@ -99,6 +99,7 @@ pub(crate) async fn upgrade( install_mirrors.python_install_mirror.as_deref(), install_mirrors.pypy_install_mirror.as_deref(), install_mirrors.python_downloads_json_url.as_deref(), + preview, ) .await? .into_interpreter(), @@ -308,7 +309,7 @@ async fn upgrade_tool( ) .await?; - let environment = installed_tools.create_environment(name, interpreter.clone())?; + let environment = installed_tools.create_environment(name, interpreter.clone(), preview)?; let environment = sync_environment( environment, diff --git a/crates/uv/src/commands/venv.rs b/crates/uv/src/commands/venv.rs index a50c0e155..fe20634d0 100644 --- a/crates/uv/src/commands/venv.rs +++ b/crates/uv/src/commands/venv.rs @@ -47,7 +47,7 @@ use super::project::default_dependency_groups; pub(crate) async fn venv( project_dir: &Path, path: Option, - python_request: Option<&str>, + python_request: Option, install_mirrors: PythonInstallMirrors, python_preference: PythonPreference, python_downloads: PythonDownloads, @@ -130,7 +130,7 @@ enum VenvError { async fn venv_impl( project_dir: &Path, path: Option, - python_request: Option<&str>, + python_request: Option, install_mirrors: PythonInstallMirrors, link_mode: LinkMode, index_locations: &IndexLocations, @@ -212,7 +212,7 @@ async fn venv_impl( python_request, requires_python, } = WorkspacePython::from_request( - python_request.map(PythonRequest::parse), + python_request, project.as_ref().map(VirtualProject::workspace), &groups, project_dir, @@ -234,6 +234,7 @@ async fn venv_impl( install_mirrors.python_install_mirror.as_deref(), install_mirrors.pypy_install_mirror.as_deref(), install_mirrors.python_downloads_json_url.as_deref(), + preview, ) .await .into_diagnostic()?; @@ -276,6 +277,11 @@ async fn venv_impl( ) .into_diagnostic()?; + let upgradeable = preview.is_enabled() + && python_request + .as_ref() + .is_none_or(|request| !request.includes_patch()); + // Create the virtual environment. let venv = uv_virtualenv::create_venv( &path, @@ -285,6 +291,8 @@ async fn venv_impl( allow_existing, relocatable, seed, + upgradeable, + preview, ) .map_err(VenvError::Creation)?; diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index 51041bcbc..fd2e28fae 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -35,6 +35,7 @@ use uv_fs::{CWD, Simplified}; use uv_pep440::release_specifiers_to_ranges; use uv_pep508::VersionOrUrl; use uv_pypi_types::{ParsedDirectoryUrl, ParsedUrl}; +use uv_python::PythonRequest; use uv_requirements::RequirementsSource; use uv_requirements_txt::RequirementsTxtRequirement; use uv_scripts::{Pep723Error, Pep723Item, Pep723ItemRef, Pep723Metadata, Pep723Script}; @@ -793,6 +794,7 @@ async fn run(mut cli: Cli) -> Result { &globals.network_settings, args.dry_run, printer, + globals.preview, ) .await } @@ -814,6 +816,7 @@ async fn run(mut cli: Cli) -> Result { args.paths, &cache, printer, + globals.preview, ) } Commands::Pip(PipNamespace { @@ -845,6 +848,7 @@ async fn run(mut cli: Cli) -> Result { args.settings.system, &cache, printer, + globals.preview, ) .await } @@ -866,6 +870,7 @@ async fn run(mut cli: Cli) -> Result { args.files, &cache, printer, + globals.preview, ) } Commands::Pip(PipNamespace { @@ -897,6 +902,7 @@ async fn run(mut cli: Cli) -> Result { args.settings.system, &cache, printer, + globals.preview, ) .await } @@ -915,6 +921,7 @@ async fn run(mut cli: Cli) -> Result { args.settings.system, &cache, printer, + globals.preview, ) } Commands::Cache(CacheNamespace { @@ -1016,10 +1023,13 @@ async fn run(mut cli: Cli) -> Result { } }); + let python_request: Option = + args.settings.python.as_deref().map(PythonRequest::parse); + commands::venv( &project_dir, args.path, - args.settings.python.as_deref(), + python_request, args.settings.install_mirrors, globals.python_preference, globals.python_downloads, @@ -1370,6 +1380,7 @@ async fn run(mut cli: Cli) -> Result { globals.python_downloads, &cache, printer, + globals.preview, ) .await } @@ -1379,12 +1390,43 @@ async fn run(mut cli: Cli) -> Result { // Resolve the settings from the command-line arguments and workspace configuration. let args = settings::PythonInstallSettings::resolve(args, filesystem); show_settings!(args); + // TODO(john): If we later want to support `--upgrade`, we need to replace this. + let upgrade = false; commands::python_install( &project_dir, args.install_dir, args.targets, args.reinstall, + upgrade, + args.force, + args.python_install_mirror, + args.pypy_install_mirror, + args.python_downloads_json_url, + globals.network_settings, + args.default, + globals.python_downloads, + cli.top_level.no_config, + globals.preview, + printer, + ) + .await + } + Commands::Python(PythonNamespace { + command: PythonCommand::Upgrade(args), + }) => { + // Resolve the settings from the command-line arguments and workspace configuration. + let args = settings::PythonUpgradeSettings::resolve(args, filesystem); + show_settings!(args); + let reinstall = false; + let upgrade = true; + + commands::python_install( + &project_dir, + args.install_dir, + args.targets, + reinstall, + upgrade, args.force, args.python_install_mirror, args.pypy_install_mirror, @@ -1433,6 +1475,7 @@ async fn run(mut cli: Cli) -> Result { cli.top_level.no_config, &cache, printer, + globals.preview, ) .await } else { @@ -1446,6 +1489,7 @@ async fn run(mut cli: Cli) -> Result { globals.python_preference, &cache, printer, + globals.preview, ) .await } @@ -1472,6 +1516,7 @@ async fn run(mut cli: Cli) -> Result { globals.network_settings, &cache, printer, + globals.preview, ) .await } diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index fb1a62b41..5cbeb1886 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -10,9 +10,9 @@ use uv_cli::{ AddArgs, ColorChoice, ExternalCommand, GlobalArgs, InitArgs, ListFormat, LockArgs, Maybe, PipCheckArgs, PipCompileArgs, PipFreezeArgs, PipInstallArgs, PipListArgs, PipShowArgs, PipSyncArgs, PipTreeArgs, PipUninstallArgs, PythonFindArgs, PythonInstallArgs, PythonListArgs, - PythonListFormat, PythonPinArgs, PythonUninstallArgs, RemoveArgs, RunArgs, SyncArgs, - ToolDirArgs, ToolInstallArgs, ToolListArgs, ToolRunArgs, ToolUninstallArgs, TreeArgs, VenvArgs, - VersionArgs, VersionBump, VersionFormat, + PythonListFormat, PythonPinArgs, PythonUninstallArgs, PythonUpgradeArgs, RemoveArgs, RunArgs, + SyncArgs, ToolDirArgs, ToolInstallArgs, ToolListArgs, ToolRunArgs, ToolUninstallArgs, TreeArgs, + VenvArgs, VersionArgs, VersionBump, VersionFormat, }; use uv_cli::{ AuthorFrom, BuildArgs, ExportArgs, PublishArgs, PythonDirArgs, ResolverInstallerArgs, @@ -973,6 +973,59 @@ impl PythonInstallSettings { } } +/// The resolved settings to use for a `python upgrade` invocation. +#[allow(clippy::struct_excessive_bools)] +#[derive(Debug, Clone)] +pub(crate) struct PythonUpgradeSettings { + pub(crate) install_dir: Option, + pub(crate) targets: Vec, + pub(crate) force: bool, + pub(crate) python_install_mirror: Option, + pub(crate) pypy_install_mirror: Option, + pub(crate) python_downloads_json_url: Option, + pub(crate) default: bool, +} + +impl PythonUpgradeSettings { + /// Resolve the [`PythonUpgradeSettings`] from the CLI and filesystem configuration. + #[allow(clippy::needless_pass_by_value)] + pub(crate) fn resolve(args: PythonUpgradeArgs, filesystem: Option) -> Self { + let options = filesystem.map(FilesystemOptions::into_options); + let (python_mirror, pypy_mirror, python_downloads_json_url) = match options { + Some(options) => ( + options.install_mirrors.python_install_mirror, + options.install_mirrors.pypy_install_mirror, + options.install_mirrors.python_downloads_json_url, + ), + None => (None, None, None), + }; + let python_mirror = args.mirror.or(python_mirror); + let pypy_mirror = args.pypy_mirror.or(pypy_mirror); + let python_downloads_json_url = + args.python_downloads_json_url.or(python_downloads_json_url); + let force = false; + let default = false; + + let PythonUpgradeArgs { + install_dir, + targets, + mirror: _, + pypy_mirror: _, + python_downloads_json_url: _, + } = args; + + Self { + install_dir, + targets, + force, + python_install_mirror: python_mirror, + pypy_install_mirror: pypy_mirror, + python_downloads_json_url, + default, + } + } +} + /// The resolved settings to use for a `python uninstall` invocation. #[derive(Debug, Clone)] pub(crate) struct PythonUninstallSettings { diff --git a/crates/uv/tests/it/common/mod.rs b/crates/uv/tests/it/common/mod.rs index 7ef7cfff6..4d65aa4a3 100644 --- a/crates/uv/tests/it/common/mod.rs +++ b/crates/uv/tests/it/common/mod.rs @@ -22,6 +22,7 @@ use regex::Regex; use tokio::io::AsyncWriteExt; use uv_cache::Cache; +use uv_configuration::PreviewMode; use uv_fs::Simplified; use uv_python::managed::ManagedPythonInstallations; use uv_python::{ @@ -959,6 +960,14 @@ impl TestContext { command } + /// Create a `uv python upgrade` command with options shared across scenarios. + pub fn python_upgrade(&self) -> Command { + let mut command = self.new_command(); + self.add_shared_options(&mut command, true); + command.arg("python").arg("upgrade"); + command + } + /// Create a `uv python pin` command with options shared across scenarios. pub fn python_pin(&self) -> Command { let mut command = self.new_command(); @@ -1434,6 +1443,7 @@ pub fn python_installations_for_versions( EnvironmentPreference::OnlySystem, PythonPreference::Managed, &cache, + PreviewMode::Disabled, ) { python.into_interpreter().sys_executable().to_owned() } else { diff --git a/crates/uv/tests/it/help.rs b/crates/uv/tests/it/help.rs index 6fd9bd466..8faebd040 100644 --- a/crates/uv/tests/it/help.rs +++ b/crates/uv/tests/it/help.rs @@ -292,6 +292,8 @@ fn help_subcommand() { Commands: list List the available Python installations install Download and install Python versions + upgrade Upgrade installed Python versions to the latest supported patch release (requires the + `--preview` flag) find Search for a Python installation pin Pin to a specific Python version dir Show the uv Python installation directory @@ -719,6 +721,8 @@ fn help_flag_subcommand() { Commands: list List the available Python installations install Download and install Python versions + upgrade Upgrade installed Python versions to the latest supported patch release (requires the + `--preview` flag) find Search for a Python installation pin Pin to a specific Python version dir Show the uv Python installation directory @@ -915,6 +919,7 @@ fn help_unknown_subsubcommand() { error: There is no command `foobar` for `uv python`. Did you mean one of: list install + upgrade find pin dir diff --git a/crates/uv/tests/it/main.rs b/crates/uv/tests/it/main.rs index 7835fa461..872c88d4b 100644 --- a/crates/uv/tests/it/main.rs +++ b/crates/uv/tests/it/main.rs @@ -84,6 +84,9 @@ mod python_install; #[cfg(feature = "python")] mod python_pin; +#[cfg(feature = "python-managed")] +mod python_upgrade; + #[cfg(all(feature = "python", feature = "pypi"))] mod run; diff --git a/crates/uv/tests/it/python_install.rs b/crates/uv/tests/it/python_install.rs index 913711c7c..d5dec1977 100644 --- a/crates/uv/tests/it/python_install.rs +++ b/crates/uv/tests/it/python_install.rs @@ -1,3 +1,6 @@ +#[cfg(windows)] +use std::path::PathBuf; + use std::{env, path::Path, process::Command}; use crate::common::{TestContext, uv_snapshot}; @@ -8,6 +11,7 @@ use assert_fs::{ use indoc::indoc; use predicates::prelude::predicate; use tracing::debug; + use uv_fs::Simplified; use uv_static::EnvVars; @@ -351,6 +355,32 @@ fn python_install_preview() { #[cfg(unix)] bin_python.assert(predicate::path::is_symlink()); + // The link should be to a path containing a minor version symlink directory + #[cfg(unix)] + { + insta::with_settings!({ + filters => context.filters(), + }, { + insta::assert_snapshot!( + &bin_python + .read_link() + .unwrap_or_else(|_| panic!("{} should be readable", bin_python.path().display())) + .as_os_str().to_string_lossy(), + @"[TEMP_DIR]/managed/cpython-3.13-[PLATFORM]/bin/python3.13" + ); + }); + } + #[cfg(windows)] + { + insta::with_settings!({ + filters => context.filters(), + }, { + insta::assert_snapshot!( + launcher_path(&bin_python).display(), @"[TEMP_DIR]/managed/cpython-3.13-[PLATFORM]/python" + ); + }); + } + // The executable should "work" uv_snapshot!(context.filters(), Command::new(bin_python.as_os_str()) .arg("-c").arg("import subprocess; print('hello world')"), @r###" @@ -459,8 +489,60 @@ fn python_install_preview() { // The executable should be removed bin_python.assert(predicate::path::missing()); + // Install a minor version + uv_snapshot!(context.filters(), context.python_install().arg("3.11").arg("--preview"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Installed Python 3.11.13 in [TIME] + + cpython-3.11.13-[PLATFORM] (python3.11) + "); + + let bin_python = context + .bin_dir + .child(format!("python3.11{}", std::env::consts::EXE_SUFFIX)); + + // The link should be to a path containing a minor version symlink directory + #[cfg(unix)] + { + insta::with_settings!({ + filters => context.filters(), + }, { + insta::assert_snapshot!( + &bin_python + .read_link() + .unwrap_or_else(|_| panic!("{} should be readable", bin_python.path().display())) + .as_os_str().to_string_lossy(), + @"[TEMP_DIR]/managed/cpython-3.11-[PLATFORM]/bin/python3.11" + ); + }); + } + #[cfg(windows)] + { + insta::with_settings!({ + filters => context.filters(), + }, { + insta::assert_snapshot!( + launcher_path(&bin_python).display(), @"[TEMP_DIR]/managed/cpython-3.11-[PLATFORM]/python" + ); + }); + } + + uv_snapshot!(context.filters(), context.python_uninstall().arg("3.11"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Searching for Python versions matching: Python 3.11 + Uninstalled Python 3.11.13 in [TIME] + - cpython-3.11.13-[PLATFORM] (python3.11) + "); + // Install multiple patch versions - uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.12.8").arg("3.12.6"), @r###" + uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.12.8").arg("3.12.6"), @r" success: true exit_code: 0 ----- stdout ----- @@ -469,13 +551,13 @@ fn python_install_preview() { Installed 2 versions in [TIME] + cpython-3.12.6-[PLATFORM] + cpython-3.12.8-[PLATFORM] (python3.12) - "###); + "); let bin_python = context .bin_dir .child(format!("python3.12{}", std::env::consts::EXE_SUFFIX)); - // The link should be for the newer patch version + // The link should resolve to the newer patch version if cfg!(unix) { insta::with_settings!({ filters => context.filters(), @@ -517,6 +599,32 @@ fn python_install_preview_upgrade() { + cpython-3.12.5-[PLATFORM] (python3.12) "###); + // Installing with a patch version should cause the link to be to the patch installation. + #[cfg(unix)] + { + insta::with_settings!({ + filters => context.filters(), + }, { + insta::assert_snapshot!( + &bin_python + .read_link() + .unwrap_or_else(|_| panic!("{} should be readable", bin_python.display())) + .display(), + @"[TEMP_DIR]/managed/cpython-3.12.5-[PLATFORM]/bin/python3.12" + ); + }); + } + #[cfg(windows)] + { + insta::with_settings!({ + filters => context.filters(), + }, { + insta::assert_snapshot!( + launcher_path(&bin_python).display(), @"[TEMP_DIR]/managed/cpython-3.12.5-[PLATFORM]/python" + ); + }); + } + // Installing 3.12.4 should not replace the executable, but also shouldn't fail uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.12.4"), @r###" success: true @@ -1023,22 +1131,25 @@ fn python_install_default() { } } -fn read_link_path(path: &Path) -> String { - if cfg!(unix) { - path.read_link() - .unwrap_or_else(|_| panic!("{} should be readable", path.display())) - .simplified_display() - .to_string() - } else if cfg!(windows) { - let launcher = uv_trampoline_builder::Launcher::try_from_path(path) - .ok() - .unwrap_or_else(|| panic!("{} should be readable", path.display())) - .unwrap_or_else(|| panic!("{} should be a valid launcher", path.display())); +#[cfg(windows)] +fn launcher_path(path: &Path) -> PathBuf { + let launcher = uv_trampoline_builder::Launcher::try_from_path(path) + .unwrap_or_else(|_| panic!("{} should be readable", path.display())) + .unwrap_or_else(|| panic!("{} should be a valid launcher", path.display())); + launcher.python_path +} - launcher.python_path.simplified_display().to_string() - } else { - unreachable!() - } +fn read_link_path(path: &Path) -> String { + #[cfg(unix)] + let canonical_path = fs_err::canonicalize(path); + + #[cfg(windows)] + let canonical_path = dunce::canonicalize(launcher_path(path)); + + canonical_path + .unwrap_or_else(|_| panic!("{} should be readable", path.display())) + .simplified_display() + .to_string() } #[test] @@ -1486,3 +1597,557 @@ fn python_install_emulated_macos() { ----- stderr ----- "); } + +// A virtual environment should track the latest patch version installed. +#[test] +fn install_transparent_patch_upgrade_uv_venv() { + let context = TestContext::new_with_versions(&["3.13"]) + .with_filtered_python_keys() + .with_filtered_exe_suffix() + .with_managed_python_dirs() + .with_filtered_python_install_bin(); + + // Install a lower patch version. + uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.12.9"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Installed Python 3.12.9 in [TIME] + + cpython-3.12.9-[PLATFORM] (python3.12) + " + ); + + // Create a virtual environment. + uv_snapshot!(context.filters(), context.venv().arg("-p").arg("3.12") + .arg(context.venv.as_os_str()), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.12.9 + Creating virtual environment at: .venv + Activate with: source .venv/[BIN]/activate + " + ); + + uv_snapshot!(context.filters(), context.run().arg("python").arg("--version"), @r" + success: true + exit_code: 0 + ----- stdout ----- + Python 3.12.9 + + ----- stderr ----- + " + ); + + // Install a higher patch version. + uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.12.11"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Installed Python 3.12.11 in [TIME] + + cpython-3.12.11-[PLATFORM] (python3.12) + " + ); + + // Virtual environment should reflect higher version. + uv_snapshot!(context.filters(), context.run().arg("python").arg("--version"), @r" + success: true + exit_code: 0 + ----- stdout ----- + Python 3.12.11 + + ----- stderr ----- + " + ); + + // Install a lower patch version. + uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.12.8"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Installed Python 3.12.8 in [TIME] + + cpython-3.12.8-[PLATFORM] + " + ); + + // Virtual environment should reflect highest version. + uv_snapshot!(context.filters(), context.run().arg("python").arg("--version"), @r" + success: true + exit_code: 0 + ----- stdout ----- + Python 3.12.11 + + ----- stderr ----- + " + ); +} + +// When installing multiple patches simultaneously, a virtual environment on that +// minor version should point to the highest. +#[test] +fn install_multiple_patches() { + let context = TestContext::new_with_versions(&[]) + .with_filtered_python_keys() + .with_filtered_exe_suffix() + .with_managed_python_dirs() + .with_filtered_python_install_bin(); + + // Install 3.12 patches in ascending order list + uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.12.9").arg("3.12.11"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Installed 2 versions in [TIME] + + cpython-3.12.9-[PLATFORM] + + cpython-3.12.11-[PLATFORM] (python3.12) + " + ); + + // Create a virtual environment. + uv_snapshot!(context.filters(), context.venv().arg("-p").arg("3.12") + .arg(context.venv.as_os_str()), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.12.11 + Creating virtual environment at: .venv + Activate with: source .venv/[BIN]/activate + " + ); + + // Virtual environment should be on highest installed patch. + uv_snapshot!(context.filters(), context.run().arg("python").arg("--version"), @r" + success: true + exit_code: 0 + ----- stdout ----- + Python 3.12.11 + + ----- stderr ----- + " + ); + + // Remove the original virtual environment + fs_err::remove_dir_all(&context.venv).unwrap(); + + // Install 3.10 patches in descending order list + uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.10.17").arg("3.10.8"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Installed 2 versions in [TIME] + + cpython-3.10.8-[PLATFORM] + + cpython-3.10.17-[PLATFORM] (python3.10) + " + ); + + // Create a virtual environment on 3.10. + uv_snapshot!(context.filters(), context.venv().arg("-p").arg("3.10") + .arg(context.venv.as_os_str()), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.10.17 + Creating virtual environment at: .venv + Activate with: source .venv/[BIN]/activate + " + ); + + // Virtual environment should be on highest installed patch. + uv_snapshot!(context.filters(), context.run().arg("python").arg("--version"), @r" + success: true + exit_code: 0 + ----- stdout ----- + Python 3.10.17 + + ----- stderr ----- + " + ); +} + +// After uninstalling the highest patch, a virtual environment should point to the +// next highest. +#[test] +fn uninstall_highest_patch() { + let context = TestContext::new_with_versions(&[]) + .with_filtered_python_keys() + .with_filtered_exe_suffix() + .with_managed_python_dirs() + .with_filtered_python_install_bin(); + + // Install patches in ascending order list + uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.12.11").arg("3.12.9").arg("3.12.8"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Installed 3 versions in [TIME] + + cpython-3.12.8-[PLATFORM] + + cpython-3.12.9-[PLATFORM] + + cpython-3.12.11-[PLATFORM] (python3.12) + " + ); + + uv_snapshot!(context.filters(), context.venv().arg("-p").arg("3.12") + .arg(context.venv.as_os_str()), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.12.11 + Creating virtual environment at: .venv + Activate with: source .venv/[BIN]/activate + " + ); + + uv_snapshot!(context.filters(), context.run().arg("python").arg("--version"), @r" + success: true + exit_code: 0 + ----- stdout ----- + Python 3.12.11 + + ----- stderr ----- + " + ); + + // Uninstall the highest patch version + uv_snapshot!(context.filters(), context.python_uninstall().arg("--preview").arg("3.12.11"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Searching for Python versions matching: Python 3.12.11 + Uninstalled Python 3.12.11 in [TIME] + - cpython-3.12.11-[PLATFORM] (python3.12) + " + ); + + // Virtual environment should be on highest patch version remaining. + uv_snapshot!(context.filters(), context.run().arg("python").arg("--version"), @r" + success: true + exit_code: 0 + ----- stdout ----- + Python 3.12.9 + + ----- stderr ----- + " + ); +} + +// Virtual environments only record minor versions. `uv venv -p 3.x.y` will +// not prevent a virtual environment from tracking the latest patch version +// installed. +#[test] +fn install_no_transparent_upgrade_with_venv_patch_specification() { + let context = TestContext::new_with_versions(&["3.13"]) + .with_filtered_python_keys() + .with_filtered_exe_suffix() + .with_managed_python_dirs() + .with_filtered_python_install_bin(); + + uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.12.9"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Installed Python 3.12.9 in [TIME] + + cpython-3.12.9-[PLATFORM] (python3.12) + " + ); + + // Create a virtual environment with a patch version + uv_snapshot!(context.filters(), context.venv().arg("-p").arg("3.12.9") + .arg(context.venv.as_os_str()), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.12.9 + Creating virtual environment at: .venv + Activate with: source .venv/[BIN]/activate + " + ); + + uv_snapshot!(context.filters(), context.run().arg("python").arg("--version"), @r" + success: true + exit_code: 0 + ----- stdout ----- + Python 3.12.9 + + ----- stderr ----- + " + ); + + // Install a higher patch version. + uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.12.11"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Installed Python 3.12.11 in [TIME] + + cpython-3.12.11-[PLATFORM] (python3.12) + " + ); + + // The virtual environment Python version is transparently upgraded. + uv_snapshot!(context.filters(), context.run().arg("python").arg("--version"), @r" + success: true + exit_code: 0 + ----- stdout ----- + Python 3.12.9 + + ----- stderr ----- + " + ); +} + +// A virtual environment created using the `venv` module should track +// the latest patch version installed. +#[test] +fn install_transparent_patch_upgrade_venv_module() { + let context = TestContext::new_with_versions(&[]) + .with_filtered_python_keys() + .with_filtered_exe_suffix() + .with_managed_python_dirs() + .with_filtered_python_install_bin(); + + let bin_dir = context.temp_dir.child("bin"); + + uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.12.9"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Installed Python 3.12.9 in [TIME] + + cpython-3.12.9-[PLATFORM] (python3.12) + " + ); + + uv_snapshot!(context.filters(), context.run().arg("python").arg("--version"), @r" + success: true + exit_code: 0 + ----- stdout ----- + Python 3.12.9 + + ----- stderr ----- + " + ); + + // Create a virtual environment using venv module. + uv_snapshot!(context.filters(), context.run().arg("python").arg("-m").arg("venv").arg(context.venv.as_os_str()).arg("--without-pip") + .env(EnvVars::PATH, bin_dir.as_os_str()), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + "); + + uv_snapshot!(context.filters(), context.run().arg("python").arg("--version"), @r" + success: true + exit_code: 0 + ----- stdout ----- + Python 3.12.9 + + ----- stderr ----- + " + ); + + // Install a higher patch version + uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.12.11"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Installed Python 3.12.11 in [TIME] + + cpython-3.12.11-[PLATFORM] (python3.12) + " + ); + + // Virtual environment should reflect highest patch version. + uv_snapshot!(context.filters(), context.run().arg("python").arg("--version"), @r" + success: true + exit_code: 0 + ----- stdout ----- + Python 3.12.11 + + ----- stderr ----- + " + ); +} + +// Automatically installing a lower patch version when running a command like +// `uv run` should not downgrade virtual environments. +#[test] +fn install_lower_patch_automatically() { + let context = TestContext::new_with_versions(&[]) + .with_filtered_python_keys() + .with_filtered_exe_suffix() + .with_managed_python_dirs() + .with_filtered_python_install_bin(); + + uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.12.11"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Installed Python 3.12.11 in [TIME] + + cpython-3.12.11-[PLATFORM] (python3.12) + " + ); + + uv_snapshot!(context.filters(), context.venv().arg("-p").arg("3.12") + .arg(context.venv.as_os_str()), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.12.11 + Creating virtual environment at: .venv + Activate with: source .venv/[BIN]/activate + " + ); + + uv_snapshot!(context.filters(), context.init().arg("-p").arg("3.12.9").arg("proj"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Initialized project `proj` at `[TEMP_DIR]/proj` + " + ); + + // Create a new virtual environment to trigger automatic installation of + // lower patch version + uv_snapshot!(context.filters(), context.venv() + .arg("--directory").arg("proj") + .arg("-p").arg("3.12.9"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.12.9 + Creating virtual environment at: .venv + Activate with: source .venv/[BIN]/activate + "); + + // Original virtual environment should still point to higher patch + uv_snapshot!(context.filters(), context.run().arg("python").arg("--version"), @r" + success: true + exit_code: 0 + ----- stdout ----- + Python 3.12.11 + + ----- stderr ----- + " + ); +} + +#[test] +fn uninstall_last_patch() { + let context = TestContext::new_with_versions(&[]) + .with_filtered_python_keys() + .with_filtered_exe_suffix() + .with_managed_python_dirs() + .with_filtered_virtualenv_bin(); + + uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.10.17"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Installed Python 3.10.17 in [TIME] + + cpython-3.10.17-[PLATFORM] (python3.10) + " + ); + + uv_snapshot!(context.filters(), context.venv().arg("-p").arg("3.10"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.10.17 + Creating virtual environment at: .venv + Activate with: source .venv/[BIN]/activate + " + ); + + uv_snapshot!(context.filters(), context.run().arg("python").arg("--version"), @r" + success: true + exit_code: 0 + ----- stdout ----- + Python 3.10.17 + + ----- stderr ----- + " + ); + + uv_snapshot!(context.filters(), context.python_uninstall().arg("--preview").arg("3.10.17"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Searching for Python versions matching: Python 3.10.17 + Uninstalled Python 3.10.17 in [TIME] + - cpython-3.10.17-[PLATFORM] (python3.10) + " + ); + + let mut filters = context.filters(); + filters.push(("python3", "python")); + + #[cfg(unix)] + uv_snapshot!(filters, context.run().arg("python").arg("--version"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Failed to inspect Python interpreter from active virtual environment at `.venv/[BIN]/python` + Caused by: Broken symlink at `.venv/[BIN]/python`, was the underlying Python interpreter removed? + + hint: Consider recreating the environment (e.g., with `uv venv`) + " + ); + + #[cfg(windows)] + uv_snapshot!(filters, context.run().arg("python").arg("--version"), @r#" + success: false + exit_code: 103 + ----- stdout ----- + + ----- stderr ----- + No Python at '"[TEMP_DIR]/managed/cpython-3.10-[PLATFORM]/python' + "# + ); +} diff --git a/crates/uv/tests/it/python_upgrade.rs b/crates/uv/tests/it/python_upgrade.rs new file mode 100644 index 000000000..cbea1d404 --- /dev/null +++ b/crates/uv/tests/it/python_upgrade.rs @@ -0,0 +1,703 @@ +use crate::common::{TestContext, uv_snapshot}; +use anyhow::Result; +use assert_fs::fixture::FileTouch; +use assert_fs::prelude::PathChild; + +use uv_static::EnvVars; + +#[test] +fn python_upgrade() { + let context: TestContext = TestContext::new_with_versions(&[]) + .with_filtered_python_keys() + .with_filtered_exe_suffix() + .with_managed_python_dirs(); + + // Install an earlier patch version + uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.10.8"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Installed Python 3.10.8 in [TIME] + + cpython-3.10.8-[PLATFORM] (python3.10) + "); + + // Don't accept patch version as argument to upgrade command + uv_snapshot!(context.filters(), context.python_upgrade().arg("--preview").arg("3.10.8"), @r" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + error: `uv python upgrade` only accepts minor versions + "); + + // Upgrade patch version + uv_snapshot!(context.filters(), context.python_upgrade().arg("--preview").arg("3.10"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Installed Python 3.10.18 in [TIME] + + cpython-3.10.18-[PLATFORM] (python3.10) + "); + + // Should be a no-op when already upgraded + uv_snapshot!(context.filters(), context.python_upgrade().arg("--preview").arg("3.10"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + "); +} + +#[test] +fn python_upgrade_without_version() { + let context: TestContext = TestContext::new_with_versions(&[]) + .with_filtered_python_keys() + .with_filtered_exe_suffix() + .with_managed_python_dirs(); + + // Should be a no-op when no versions have been installed + uv_snapshot!(context.filters(), context.python_upgrade().arg("--preview"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + There are no installed versions to upgrade + "); + + // Install earlier patch versions for different minor versions + uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.11.8").arg("3.12.8").arg("3.13.1"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Installed 3 versions in [TIME] + + cpython-3.11.8-[PLATFORM] (python3.11) + + cpython-3.12.8-[PLATFORM] (python3.12) + + cpython-3.13.1-[PLATFORM] (python3.13) + "); + + let mut filters = context.filters().clone(); + filters.push((r"3.13.\d+", "3.13.[X]")); + + // Upgrade one patch version + uv_snapshot!(filters, context.python_upgrade().arg("--preview").arg("3.13"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Installed Python 3.13.[X] in [TIME] + + cpython-3.13.[X]-[PLATFORM] (python3.13) + "); + + // Providing no minor version to `uv python upgrade` should upgrade the rest + // of the patch versions + uv_snapshot!(context.filters(), context.python_upgrade().arg("--preview"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Installed 2 versions in [TIME] + + cpython-3.11.13-[PLATFORM] (python3.11) + + cpython-3.12.11-[PLATFORM] (python3.12) + "); + + // Should be a no-op when every version is already upgraded + uv_snapshot!(context.filters(), context.python_upgrade().arg("--preview"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + All versions already on latest supported patch release + "); +} + +#[test] +fn python_upgrade_transparent_from_venv() { + let context: TestContext = TestContext::new_with_versions(&["3.13"]) + .with_filtered_python_keys() + .with_filtered_exe_suffix() + .with_managed_python_dirs(); + + // Install an earlier patch version + uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.10.8"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Installed Python 3.10.8 in [TIME] + + cpython-3.10.8-[PLATFORM] (python3.10) + "); + + // Create a virtual environment + uv_snapshot!(context.filters(), context.venv(), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.10.8 + Creating virtual environment at: .venv + Activate with: source .venv/[BIN]/activate + "); + + uv_snapshot!(context.filters(), context.run().arg("python").arg("--version"), @r" + success: true + exit_code: 0 + ----- stdout ----- + Python 3.10.8 + + ----- stderr ----- + " + ); + + let second_venv = ".venv2"; + + // Create a second virtual environment with minor version request + uv_snapshot!(context.filters(), context.venv().arg(second_venv).arg("-p").arg("3.10"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.10.8 + Creating virtual environment at: .venv2 + Activate with: source .venv2/[BIN]/activate + "); + + uv_snapshot!(context.filters(), context.run().arg("python").arg("--version") + .env(EnvVars::VIRTUAL_ENV, second_venv), @r" + success: true + exit_code: 0 + ----- stdout ----- + Python 3.10.8 + + ----- stderr ----- + " + ); + + // Upgrade patch version + uv_snapshot!(context.filters(), context.python_upgrade().arg("--preview").arg("3.10"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Installed Python 3.10.18 in [TIME] + + cpython-3.10.18-[PLATFORM] (python3.10) + "); + + // First virtual environment should reflect upgraded patch + uv_snapshot!(context.filters(), context.run().arg("python").arg("--version"), @r" + success: true + exit_code: 0 + ----- stdout ----- + Python 3.10.18 + + ----- stderr ----- + " + ); + + // Second virtual environment should reflect upgraded patch + uv_snapshot!(context.filters(), context.run().arg("python").arg("--version") + .env(EnvVars::VIRTUAL_ENV, second_venv), @r" + success: true + exit_code: 0 + ----- stdout ----- + Python 3.10.18 + + ----- stderr ----- + " + ); +} + +// Installing Python in preview mode should not prevent virtual environments +// from transparently upgrading. +#[test] +fn python_upgrade_transparent_from_venv_preview() { + let context: TestContext = TestContext::new_with_versions(&["3.13"]) + .with_filtered_python_keys() + .with_filtered_exe_suffix() + .with_managed_python_dirs(); + + // Install an earlier patch version using `--preview` + uv_snapshot!(context.filters(), context.python_install().arg("3.10.8").arg("--preview"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Installed Python 3.10.8 in [TIME] + + cpython-3.10.8-[PLATFORM] (python3.10) + "); + + // Create a virtual environment + uv_snapshot!(context.filters(), context.venv().arg("-p").arg("3.10"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.10.8 + Creating virtual environment at: .venv + Activate with: source .venv/[BIN]/activate + "); + + uv_snapshot!(context.filters(), context.run().arg("python").arg("--version"), @r" + success: true + exit_code: 0 + ----- stdout ----- + Python 3.10.8 + + ----- stderr ----- + " + ); + + // Upgrade patch version + uv_snapshot!(context.filters(), context.python_upgrade().arg("--preview").arg("3.10"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Installed Python 3.10.18 in [TIME] + + cpython-3.10.18-[PLATFORM] (python3.10) + "); + + // Virtual environment should reflect upgraded patch + uv_snapshot!(context.filters(), context.run().arg("python").arg("--version"), @r" + success: true + exit_code: 0 + ----- stdout ----- + Python 3.10.18 + + ----- stderr ----- + " + ); +} + +#[test] +fn python_upgrade_ignored_with_python_pin() { + let context: TestContext = TestContext::new_with_versions(&["3.13"]) + .with_filtered_python_keys() + .with_filtered_exe_suffix() + .with_managed_python_dirs(); + + // Install an earlier patch version + uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.10.8"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Installed Python 3.10.8 in [TIME] + + cpython-3.10.8-[PLATFORM] (python3.10) + "); + + // Create a virtual environment + uv_snapshot!(context.filters(), context.venv(), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.10.8 + Creating virtual environment at: .venv + Activate with: source .venv/[BIN]/activate + "); + + // Pin to older patch version + uv_snapshot!(context.filters(), context.python_pin().arg("3.10.8"), @r" + success: true + exit_code: 0 + ----- stdout ----- + Pinned `.python-version` to `3.10.8` + + ----- stderr ----- + "); + + // Upgrade patch version + uv_snapshot!(context.filters(), context.python_upgrade().arg("--preview").arg("3.10"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Installed Python 3.10.18 in [TIME] + + cpython-3.10.18-[PLATFORM] (python3.10) + "); + + // Virtual environment should continue to respect pinned patch version + uv_snapshot!(context.filters(), context.run().arg("python").arg("--version"), @r" + success: true + exit_code: 0 + ----- stdout ----- + Python 3.10.8 + + ----- stderr ----- + " + ); +} + +// Virtual environments only record minor versions. `uv venv -p 3.x.y` will +// not prevent transparent upgrades. +#[test] +fn python_no_transparent_upgrade_with_venv_patch_specification() { + let context: TestContext = TestContext::new_with_versions(&["3.13"]) + .with_filtered_python_keys() + .with_filtered_exe_suffix() + .with_managed_python_dirs(); + + // Install an earlier patch version + uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.10.8"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Installed Python 3.10.8 in [TIME] + + cpython-3.10.8-[PLATFORM] (python3.10) + "); + + // Create a virtual environment with a patch version + uv_snapshot!(context.filters(), context.venv().arg("-p").arg("3.10.8"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.10.8 + Creating virtual environment at: .venv + Activate with: source .venv/[BIN]/activate + "); + + uv_snapshot!(context.filters(), context.run().arg("python").arg("--version"), @r" + success: true + exit_code: 0 + ----- stdout ----- + Python 3.10.8 + + ----- stderr ----- + " + ); + + // Upgrade patch version + uv_snapshot!(context.filters(), context.python_upgrade().arg("--preview").arg("3.10"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Installed Python 3.10.18 in [TIME] + + cpython-3.10.18-[PLATFORM] (python3.10) + "); + + // The virtual environment Python version is transparently upgraded. + uv_snapshot!(context.filters(), context.run().arg("python").arg("--version"), @r" + success: true + exit_code: 0 + ----- stdout ----- + Python 3.10.8 + + ----- stderr ----- + " + ); +} + +// Transparent upgrades should work for virtual environments created within +// virtual environments. +#[test] +fn python_transparent_upgrade_venv_venv() { + let context: TestContext = TestContext::new_with_versions(&["3.13"]) + .with_filtered_python_keys() + .with_filtered_exe_suffix() + .with_filtered_virtualenv_bin() + .with_managed_python_dirs(); + + // Install an earlier patch version + uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.10.8"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Installed Python 3.10.8 in [TIME] + + cpython-3.10.8-[PLATFORM] (python3.10) + "); + + // Create an initial virtual environment + uv_snapshot!(context.filters(), context.venv().arg("-p").arg("3.10"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.10.8 + Creating virtual environment at: .venv + Activate with: source .venv/[BIN]/activate + "); + + let venv_python = if cfg!(windows) { + context.venv.child("Scripts/python.exe") + } else { + context.venv.child("bin/python") + }; + + let second_venv = ".venv2"; + + // Create a new virtual environment from within a virtual environment + uv_snapshot!(context.filters(), context.venv() + .arg(second_venv) + .arg("-p").arg(venv_python.as_os_str()), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.10.8 interpreter at: .venv/[BIN]/python + Creating virtual environment at: .venv2 + Activate with: source .venv2/[BIN]/activate + "); + + // Check version from within second virtual environment + uv_snapshot!(context.filters(), context.run() + .arg("python").arg("--version") + .env(EnvVars::VIRTUAL_ENV, second_venv), @r" + success: true + exit_code: 0 + ----- stdout ----- + Python 3.10.8 + + ----- stderr ----- + " + ); + + // Upgrade patch version + uv_snapshot!(context.filters(), context.python_upgrade().arg("--preview").arg("3.10"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Installed Python 3.10.18 in [TIME] + + cpython-3.10.18-[PLATFORM] (python3.10) + "); + + // Should have transparently upgraded in second virtual environment + uv_snapshot!(context.filters(), context.run() + .arg("python").arg("--version") + .env(EnvVars::VIRTUAL_ENV, second_venv), @r" + success: true + exit_code: 0 + ----- stdout ----- + Python 3.10.18 + + ----- stderr ----- + " + ); +} + +// Transparent upgrades should work for virtual environments created using +// the `venv` module. +#[test] +fn python_upgrade_transparent_from_venv_module() { + let context = TestContext::new_with_versions(&["3.13"]) + .with_filtered_python_keys() + .with_filtered_exe_suffix() + .with_managed_python_dirs() + .with_filtered_python_install_bin(); + + let bin_dir = context.temp_dir.child("bin"); + + // Install earlier patch version + uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.12.9"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Installed Python 3.12.9 in [TIME] + + cpython-3.12.9-[PLATFORM] (python3.12) + "); + + // Create a virtual environment using venv module + uv_snapshot!(context.filters(), context.run().arg("python").arg("-m").arg("venv").arg(context.venv.as_os_str()).arg("--without-pip") + .env(EnvVars::PATH, bin_dir.as_os_str()), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + "); + + uv_snapshot!(context.filters(), context.run().arg("python").arg("--version"), @r" + success: true + exit_code: 0 + ----- stdout ----- + Python 3.12.9 + + ----- stderr ----- + " + ); + + // Upgrade patch version + uv_snapshot!(context.filters(), context.python_upgrade().arg("--preview").arg("3.12"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Installed Python 3.12.11 in [TIME] + + cpython-3.12.11-[PLATFORM] (python3.12) + " + ); + + // Virtual environment should reflect upgraded patch + uv_snapshot!(context.filters(), context.run().arg("python").arg("--version"), @r" + success: true + exit_code: 0 + ----- stdout ----- + Python 3.12.11 + + ----- stderr ----- + " + ); +} + +// Transparent Python upgrades should work in environments created using +// the `venv` module within an existing virtual environment. +#[test] +fn python_upgrade_transparent_from_venv_module_in_venv() { + let context = TestContext::new_with_versions(&["3.13"]) + .with_filtered_python_keys() + .with_filtered_exe_suffix() + .with_managed_python_dirs() + .with_filtered_python_install_bin(); + + let bin_dir = context.temp_dir.child("bin"); + + // Install earlier patch version + uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.10.8"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Installed Python 3.10.8 in [TIME] + + cpython-3.10.8-[PLATFORM] (python3.10) + "); + + // Create first virtual environment + uv_snapshot!(context.filters(), context.venv().arg("-p").arg("3.10"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.10.8 + Creating virtual environment at: .venv + Activate with: source .venv/[BIN]/activate + "); + + let second_venv = ".venv2"; + + // Create a virtual environment using `venv`` module from within the first virtual environment. + uv_snapshot!(context.filters(), context.run() + .arg("python").arg("-m").arg("venv").arg(second_venv).arg("--without-pip") + .env(EnvVars::PATH, bin_dir.as_os_str()), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + "); + + // Check version within second virtual environment + uv_snapshot!(context.filters(), context.run() + .env(EnvVars::VIRTUAL_ENV, second_venv) + .arg("python").arg("--version"), @r" + success: true + exit_code: 0 + ----- stdout ----- + Python 3.10.8 + + ----- stderr ----- + " + ); + + // Upgrade patch version + uv_snapshot!(context.filters(), context.python_upgrade().arg("--preview").arg("3.10"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Installed Python 3.10.18 in [TIME] + + cpython-3.10.18-[PLATFORM] (python3.10) + " + ); + + // Second virtual environment should reflect upgraded patch. + uv_snapshot!(context.filters(), context.run() + .env(EnvVars::VIRTUAL_ENV, second_venv) + .arg("python").arg("--version"), @r" + success: true + exit_code: 0 + ----- stdout ----- + Python 3.10.18 + + ----- stderr ----- + " + ); +} + +// Tests that `uv python upgrade 3.12` will warn if trying to install over non-managed +// interpreter. +#[test] +fn python_upgrade_force_install() -> Result<()> { + let context = TestContext::new_with_versions(&["3.13"]) + .with_filtered_python_keys() + .with_filtered_exe_suffix() + .with_managed_python_dirs(); + + context + .bin_dir + .child(format!("python3.12{}", std::env::consts::EXE_SUFFIX)) + .touch()?; + + // Try to upgrade with a non-managed interpreter installed in `bin`. + uv_snapshot!(context.filters(), context.python_upgrade().arg("--preview").arg("3.12"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: Executable already exists at `[BIN]/python3.12` but is not managed by uv; use `uv python install 3.12 --force` to replace it + Installed Python 3.12.11 in [TIME] + + cpython-3.12.11-[PLATFORM] + "); + + // Force the `bin` install. + uv_snapshot!(context.filters(), context.python_install().arg("3.12").arg("--force").arg("--preview").arg("3.12"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Installed Python 3.12.11 in [TIME] + + cpython-3.12.11-[PLATFORM] (python3.12) + "); + + Ok(()) +} diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index 966dd41d2..e6fe831d3 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -9619,9 +9619,7 @@ fn sync_when_virtual_environment_incompatible_with_interpreter() -> Result<()> { }, { let contents = fs_err::read_to_string(&pyvenv_cfg).unwrap(); let lines: Vec<&str> = contents.split('\n').collect(); - assert_snapshot!(lines[3], @r###" - version_info = 3.12.[X] - "###); + assert_snapshot!(lines[3], @"version_info = 3.12.[X]"); }); // Simulate an incompatible `pyvenv.cfg:version_info` value created @@ -9660,9 +9658,7 @@ fn sync_when_virtual_environment_incompatible_with_interpreter() -> Result<()> { }, { let contents = fs_err::read_to_string(&pyvenv_cfg).unwrap(); let lines: Vec<&str> = contents.split('\n').collect(); - assert_snapshot!(lines[3], @r###" - version_info = 3.12.[X] - "###); + assert_snapshot!(lines[3], @"version_info = 3.12.[X]"); }); Ok(()) diff --git a/crates/uv/tests/it/tool_upgrade.rs b/crates/uv/tests/it/tool_upgrade.rs index a36db9b92..70309f04d 100644 --- a/crates/uv/tests/it/tool_upgrade.rs +++ b/crates/uv/tests/it/tool_upgrade.rs @@ -741,9 +741,7 @@ fn tool_upgrade_python() { }, { let content = fs_err::read_to_string(tool_dir.join("babel").join("pyvenv.cfg")).unwrap(); let lines: Vec<&str> = content.split('\n').collect(); - assert_snapshot!(lines[lines.len() - 3], @r###" - version_info = 3.12.[X] - "###); + assert_snapshot!(lines[lines.len() - 3], @"version_info = 3.12.[X]"); }); } @@ -826,9 +824,7 @@ fn tool_upgrade_python_with_all() { }, { let content = fs_err::read_to_string(tool_dir.join("babel").join("pyvenv.cfg")).unwrap(); let lines: Vec<&str> = content.split('\n').collect(); - assert_snapshot!(lines[lines.len() - 3], @r###" - version_info = 3.12.[X] - "###); + assert_snapshot!(lines[lines.len() - 3], @"version_info = 3.12.[X]"); }); insta::with_settings!({ @@ -836,8 +832,6 @@ fn tool_upgrade_python_with_all() { }, { let content = fs_err::read_to_string(tool_dir.join("python-dotenv").join("pyvenv.cfg")).unwrap(); let lines: Vec<&str> = content.split('\n').collect(); - assert_snapshot!(lines[lines.len() - 3], @r###" - version_info = 3.12.[X] - "###); + assert_snapshot!(lines[lines.len() - 3], @"version_info = 3.12.[X]"); }); } diff --git a/crates/uv/tests/it/venv.rs b/crates/uv/tests/it/venv.rs index bc35f9490..f1860efa2 100644 --- a/crates/uv/tests/it/venv.rs +++ b/crates/uv/tests/it/venv.rs @@ -868,14 +868,14 @@ fn create_venv_unknown_python_patch() { "### ); } else { - uv_snapshot!(&mut command, @r###" + uv_snapshot!(&mut command, @r" success: false exit_code: 1 ----- stdout ----- ----- stderr ----- × No interpreter found for Python 3.12.100 in managed installations or search path - "### + " ); } diff --git a/docs/concepts/python-versions.md b/docs/concepts/python-versions.md index 0d409ff50..a7472bea8 100644 --- a/docs/concepts/python-versions.md +++ b/docs/concepts/python-versions.md @@ -123,7 +123,7 @@ present, uv will install all the Python versions listed in the file. !!! important - Support for installing Python executables is in _preview_, this means the behavior is experimental + Support for installing Python executables is in _preview_. This means the behavior is experimental and subject to change. To install Python executables into your `PATH`, provide the `--preview` option: @@ -158,6 +158,70 @@ $ uv python install 3.12.6 --preview # Does not update `python3.12` $ uv python install 3.12.8 --preview # Updates `python3.12` to point to 3.12.8 ``` +## Upgrading Python versions + +!!! important + + Support for upgrading Python versions is in _preview_. This means the behavior is experimental + and subject to change. + + Upgrades are only supported for uv-managed Python versions. + + Upgrades are not currently supported for PyPy and GraalPy. + +uv allows transparently upgrading Python versions to the latest patch release, e.g., 3.13.4 to +3.13.5. uv does not allow transparently upgrading across minor Python versions, e.g., 3.12 to 3.13, +because changing minor versions can affect dependency resolution. + +uv-managed Python versions can be upgraded to the latest supported patch release with the +`python upgrade` command: + +To upgrade a Python version to the latest supported patch release: + +```console +$ uv python upgrade 3.12 +``` + +To upgrade all installed Python versions: + +```console +$ uv python upgrade +``` + +After an upgrade, uv will prefer the new version, but will retain the existing version as it may +still be used by virtual environments. + +If the Python version was installed with preview enabled, e.g., `uv python install 3.12 --preview`, +virtual environments using the Python version will be automatically upgraded to the new patch +version. + +!!! note + + If the virtual environment was created _before_ opting in to the preview mode, it will not be + included in the automatic upgrades. + +If a virtual environment was created with an explicitly requested patch version, e.g., +`uv venv -p 3.10.8`, it will not be transparently upgraded to a new version. + +### Minor version directories + +Automatic upgrades for virtual environments are implemented using a directory with the Python minor +version, e.g.: + +``` +~/.local/share/uv/python/cpython-3.12-macos-aarch64-none +``` + +which is a symbolic link (on Unix) or junction (on Windows) pointing to a specific patch version: + +```console +$ readlink ~/.local/share/uv/python/cpython-3.12-macos-aarch64-none +~/.local/share/uv/python/cpython-3.12.11-macos-aarch64-none +``` + +If this link is resolved by another tool, e.g., by canonicalizing the Python interpreter path, and +used to create a virtual environment, it will not be automatically upgraded. + ## Project Python versions uv will respect Python requirements defined in `requires-python` in the `pyproject.toml` file during diff --git a/docs/guides/install-python.md b/docs/guides/install-python.md index 0b80589a5..da841eac6 100644 --- a/docs/guides/install-python.md +++ b/docs/guides/install-python.md @@ -120,6 +120,28 @@ To force uv to use the system Python, provide the `--no-managed-python` flag. Se [Python version preference](../concepts/python-versions.md#requiring-or-disabling-managed-python-versions) documentation for more details. +## Upgrading Python versions + +!!! important + + Support for upgrading Python patch versions is in _preview_. This means the behavior is + experimental and subject to change. + +To upgrade a Python version to the latest supported patch release: + +```console +$ uv python upgrade 3.12 +``` + +To upgrade all uv-managed Python versions: + +```console +$ uv python upgrade +``` + +See the [`python upgrade`](../concepts/python-versions.md#upgrading-python-versions) documentation +for more details. + ## Next steps To learn more about `uv python`, see the [Python version concept](../concepts/python-versions.md) diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 9ae05a8e0..48b0351fe 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -2559,6 +2559,7 @@ uv python [OPTIONS]

    uv python list

    List the available Python installations

    uv python install

    Download and install Python versions

    +
    uv python upgrade

    Upgrade installed Python versions to the latest supported patch release (requires the --preview flag)

    uv python find

    Search for a Python installation

    uv python pin

    Pin to a specific Python version

    uv python dir

    Show the uv Python installation directory

    @@ -2753,6 +2754,91 @@ uv python install [OPTIONS] [TARGETS]...

    You can configure fine-grained logging using the RUST_LOG environment variable. (https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives)

    +### uv python upgrade + +Upgrade installed Python versions to the latest supported patch release (requires the `--preview` flag). + +A target Python minor version to upgrade may be provided, e.g., `3.13`. Multiple versions may be provided to perform more than one upgrade. + +If no target version is provided, then uv will upgrade all managed CPython versions. + +During an upgrade, uv will not uninstall outdated patch versions. + +When an upgrade is performed, virtual environments created by uv will automatically use the new version. However, if the virtual environment was created before the upgrade functionality was added, it will continue to use the old Python version; to enable upgrades, the environment must be recreated. + +Upgrades are not yet supported for alternative implementations, like PyPy. + +

    Usage

    + +``` +uv python upgrade [OPTIONS] [TARGETS]... +``` + +

    Arguments

    + +
    TARGETS

    The Python minor version(s) to upgrade.

    +

    If no target version is provided, then uv will upgrade all managed CPython versions.

    +
    + +

    Options

    + +
    --allow-insecure-host, --trusted-host allow-insecure-host

    Allow insecure connections to a host.

    +

    Can be provided multiple times.

    +

    Expects to receive either a hostname (e.g., localhost), a host-port pair (e.g., localhost:8080), or a URL (e.g., https://localhost).

    +

    WARNING: Hosts included in this list will not be verified against the system's certificate store. Only use --allow-insecure-host in a secure network with verified sources, as it bypasses SSL verification and could expose you to MITM attacks.

    +

    May also be set with the UV_INSECURE_HOST environment variable.

    --cache-dir cache-dir

    Path to the cache directory.

    +

    Defaults to $XDG_CACHE_HOME/uv or $HOME/.cache/uv on macOS and Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    +

    To view the location of the cache directory, run uv cache dir.

    +

    May also be set with the UV_CACHE_DIR environment variable.

    --color color-choice

    Control the use of color in output.

    +

    By default, uv will automatically detect support for colors when writing to a terminal.

    +

    Possible values:

    +
      +
    • auto: Enables colored output only when the output is going to a terminal or TTY with support
    • +
    • always: Enables colored output regardless of the detected environment
    • +
    • never: Disables colored output
    • +
    --config-file config-file

    The path to a uv.toml file to use for configuration.

    +

    While uv configuration can be included in a pyproject.toml file, it is not allowed in this context.

    +

    May also be set with the UV_CONFIG_FILE environment variable.

    --directory directory

    Change to the given directory prior to running the command.

    +

    Relative paths are resolved with the given directory as the base.

    +

    See --project to only change the project root directory.

    +
    --help, -h

    Display the concise help for this command

    +
    --install-dir, -i install-dir

    The directory Python installations are stored in.

    +

    If provided, UV_PYTHON_INSTALL_DIR will need to be set for subsequent operations for uv to discover the Python installation.

    +

    See uv python dir to view the current Python installation directory. Defaults to ~/.local/share/uv/python.

    +

    May also be set with the UV_PYTHON_INSTALL_DIR environment variable.

    --managed-python

    Require use of uv-managed Python versions.

    +

    By default, uv prefers using Python versions it manages. However, it will use system Python versions if a uv-managed Python is not installed. This option disables use of system Python versions.

    +

    May also be set with the UV_MANAGED_PYTHON environment variable.

    --mirror mirror

    Set the URL to use as the source for downloading Python installations.

    +

    The provided URL will replace https://github.com/astral-sh/python-build-standalone/releases/download in, e.g., https://github.com/astral-sh/python-build-standalone/releases/download/20240713/cpython-3.12.4%2B20240713-aarch64-apple-darwin-install_only.tar.gz.

    +

    Distributions can be read from a local directory by using the file:// URL scheme.

    +

    May also be set with the UV_PYTHON_INSTALL_MIRROR environment variable.

    --native-tls

    Whether to load TLS certificates from the platform's native certificate store.

    +

    By default, uv loads certificates from the bundled webpki-roots crate. The webpki-roots are a reliable set of trust roots from Mozilla, and including them in uv improves portability and performance (especially on macOS).

    +

    However, in some cases, you may want to use the platform's native certificate store, especially if you're relying on a corporate trust root (e.g., for a mandatory proxy) that's included in your system's certificate store.

    +

    May also be set with the UV_NATIVE_TLS environment variable.

    --no-cache, --no-cache-dir, -n

    Avoid reading from or writing to the cache, instead using a temporary directory for the duration of the operation

    +

    May also be set with the UV_NO_CACHE environment variable.

    --no-config

    Avoid discovering configuration files (pyproject.toml, uv.toml).

    +

    Normally, configuration files are discovered in the current directory, parent directories, or user configuration directories.

    +

    May also be set with the UV_NO_CONFIG environment variable.

    --no-managed-python

    Disable use of uv-managed Python versions.

    +

    Instead, uv will search for a suitable Python version on the system.

    +

    May also be set with the UV_NO_MANAGED_PYTHON environment variable.

    --no-progress

    Hide all progress outputs.

    +

    For example, spinners or progress bars.

    +

    May also be set with the UV_NO_PROGRESS environment variable.

    --no-python-downloads

    Disable automatic downloads of Python.

    +
    --offline

    Disable network access.

    +

    When disabled, uv will only use locally cached data and locally available files.

    +

    May also be set with the UV_OFFLINE environment variable.

    --project project

    Run the command within the given project directory.

    +

    All pyproject.toml, uv.toml, and .python-version files will be discovered by walking up the directory tree from the project root, as will the project's virtual environment (.venv).

    +

    Other command-line arguments (such as relative paths) will be resolved relative to the current working directory.

    +

    See --directory to change the working directory entirely.

    +

    This setting has no effect when used in the uv pip interface.

    +

    May also be set with the UV_PROJECT environment variable.

    --pypy-mirror pypy-mirror

    Set the URL to use as the source for downloading PyPy installations.

    +

    The provided URL will replace https://downloads.python.org/pypy in, e.g., https://downloads.python.org/pypy/pypy3.8-v7.3.7-osx64.tar.bz2.

    +

    Distributions can be read from a local directory by using the file:// URL scheme.

    +

    May also be set with the UV_PYPY_INSTALL_MIRROR environment variable.

    --python-downloads-json-url python-downloads-json-url

    URL pointing to JSON of custom Python installations.

    +

    Note that currently, only local paths are supported.

    +

    May also be set with the UV_PYTHON_DOWNLOADS_JSON_URL environment variable.

    --quiet, -q

    Use quiet output.

    +

    Repeating this option, e.g., -qq, will enable a silent mode in which uv will write no output to stdout.

    +
    --verbose, -v

    Use verbose output.

    +

    You can configure fine-grained logging using the RUST_LOG environment variable. (https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives)

    +
    + ### uv python find Search for a Python installation. From c710246d76a583cfee6aa925ec72aceb1bf86bce Mon Sep 17 00:00:00 2001 From: Aria Desires Date: Fri, 20 Jun 2025 14:20:01 -0400 Subject: [PATCH 053/185] Update cargo-dist (#14156) Also took the time to migrate to the external config format to normalize our projects for team comfort (`ty` *has* to use this format for its workspace structure). --- .github/workflows/release.yml | 2 +- Cargo.toml | 83 -------------------------------- dist-workspace.toml | 89 +++++++++++++++++++++++++++++++++++ 3 files changed, 90 insertions(+), 84 deletions(-) create mode 100644 dist-workspace.toml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b1c77c316..2688c3fc8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -69,7 +69,7 @@ jobs: # we specify bash to get pipefail; it guards against the `curl` command # failing. otherwise `sh` won't catch that `curl` returned non-0 shell: bash - run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/cargo-dist/releases/download/v0.28.4/cargo-dist-installer.sh | sh" + run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/cargo-dist/releases/download/v0.28.7-prerelease.1/cargo-dist-installer.sh | sh" - name: Cache dist uses: actions/upload-artifact@6027e3dd177782cd8ab9af838c04fd81a07f1d47 with: diff --git a/Cargo.toml b/Cargo.toml index c6d5729e2..4269c6cfa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -297,89 +297,6 @@ codegen-units = 1 [profile.dist] inherits = "release" -# Config for 'dist' -[workspace.metadata.dist] -# The preferred dist version to use in CI (Cargo.toml SemVer syntax) -cargo-dist-version = "0.28.4" -# make a package being included in our releases opt-in instead of opt-out -dist = false -# CI backends to support -ci = "github" -# The installers to generate for each app -installers = ["shell", "powershell"] -# The archive format to use for windows builds (defaults .zip) -windows-archive = ".zip" -# The archive format to use for non-windows builds (defaults .tar.xz) -unix-archive = ".tar.gz" -# Target platforms to build apps for (Rust target-triple syntax) -targets = [ - "aarch64-apple-darwin", - "aarch64-pc-windows-msvc", - "aarch64-unknown-linux-gnu", - "aarch64-unknown-linux-musl", - "arm-unknown-linux-musleabihf", - "armv7-unknown-linux-gnueabihf", - "armv7-unknown-linux-musleabihf", - "i686-pc-windows-msvc", - "i686-unknown-linux-gnu", - "i686-unknown-linux-musl", - "powerpc64-unknown-linux-gnu", - "powerpc64le-unknown-linux-gnu", - "riscv64gc-unknown-linux-gnu", - "s390x-unknown-linux-gnu", - "x86_64-apple-darwin", - "x86_64-pc-windows-msvc", - "x86_64-unknown-linux-gnu", - "x86_64-unknown-linux-musl", -] -# Whether to auto-include files like READMEs, LICENSEs, and CHANGELOGs (default true) -auto-includes = false -# Whether dist should create a Github Release or use an existing draft -create-release = true -# Which actions to run on pull requests -pr-run-mode = "plan" -# Whether CI should trigger releases with dispatches instead of tag pushes -dispatch-releases = true -# Which phase dist should use to create the GitHub release -github-release = "announce" -# Whether CI should include auto-generated code to build local artifacts -build-local-artifacts = false -# Local artifacts jobs to run in CI -local-artifacts-jobs = ["./build-binaries", "./build-docker"] -# Publish jobs to run in CI -publish-jobs = ["./publish-pypi"] -# Post-announce jobs to run in CI -post-announce-jobs = ["./publish-docs"] -# Custom permissions for GitHub Jobs -github-custom-job-permissions = { "build-docker" = { packages = "write", contents = "read", id-token = "write", attestations = "write" } } -# Whether to install an updater program -install-updater = false -# Path that installers should place binaries in -install-path = ["$XDG_BIN_HOME/", "$XDG_DATA_HOME/../bin", "~/.local/bin"] - -[workspace.metadata.dist.github-custom-runners] -global = "depot-ubuntu-latest-4" - -[workspace.metadata.dist.min-glibc-version] -# Override glibc version for specific target triplets. -aarch64-unknown-linux-gnu = "2.28" -riscv64gc-unknown-linux-gnu = "2.31" -# Override all remaining glibc versions. -"*" = "2.17" - -[workspace.metadata.dist.github-action-commits] -"actions/checkout" = "11bd71901bbe5b1630ceea73d27597364c9af683" # v4 -"actions/upload-artifact" = "6027e3dd177782cd8ab9af838c04fd81a07f1d47" # v4.6.2 -"actions/download-artifact" = "d3f86a106a0bac45b974a628896c90dbdf5c8093" # v4.3.0 -"actions/attest-build-provenance" = "c074443f1aee8d4aeeae555aebba3282517141b2" #v2.2.3 - [patch.crates-io] reqwest-middleware = { git = "https://github.com/astral-sh/reqwest-middleware", rev = "ad8b9d332d1773fde8b4cd008486de5973e0a3f8" } reqwest-retry = { git = "https://github.com/astral-sh/reqwest-middleware", rev = "ad8b9d332d1773fde8b4cd008486de5973e0a3f8" } - -[workspace.metadata.dist.binaries] -"*" = ["uv", "uvx"] -# Add "uvw" binary for Windows targets -aarch64-pc-windows-msvc = ["uv", "uvx", "uvw"] -i686-pc-windows-msvc = ["uv", "uvx", "uvw"] -x86_64-pc-windows-msvc = ["uv", "uvx", "uvw"] diff --git a/dist-workspace.toml b/dist-workspace.toml new file mode 100644 index 000000000..3e16bd4cf --- /dev/null +++ b/dist-workspace.toml @@ -0,0 +1,89 @@ +[workspace] +members = ["cargo:."] + +# Config for 'dist' +[dist] +# The preferred dist version to use in CI (Cargo.toml SemVer syntax) +cargo-dist-version = "0.28.7-prerelease.1" +# make a package being included in our releases opt-in instead of opt-out +dist = false +# CI backends to support +ci = "github" +# The installers to generate for each app +installers = ["shell", "powershell"] +# The archive format to use for windows builds (defaults .zip) +windows-archive = ".zip" +# The archive format to use for non-windows builds (defaults .tar.xz) +unix-archive = ".tar.gz" +# Target platforms to build apps for (Rust target-triple syntax) +targets = [ + "aarch64-apple-darwin", + "aarch64-unknown-linux-gnu", + "aarch64-unknown-linux-musl", + "aarch64-pc-windows-msvc", + "arm-unknown-linux-musleabihf", + "armv7-unknown-linux-gnueabihf", + "armv7-unknown-linux-musleabihf", + "x86_64-apple-darwin", + "powerpc64-unknown-linux-gnu", + "powerpc64le-unknown-linux-gnu", + "riscv64gc-unknown-linux-gnu", + "s390x-unknown-linux-gnu", + "x86_64-unknown-linux-gnu", + "x86_64-unknown-linux-musl", + "x86_64-pc-windows-msvc", + "i686-unknown-linux-gnu", + "i686-unknown-linux-musl", + "i686-pc-windows-msvc" +] +# Whether to auto-include files like READMEs, LICENSEs, and CHANGELOGs (default true) +auto-includes = false +# Whether dist should create a Github Release or use an existing draft +create-release = true +# Which actions to run on pull requests +pr-run-mode = "plan" +# Whether CI should trigger releases with dispatches instead of tag pushes +dispatch-releases = true +# Which phase dist should use to create the GitHub release +github-release = "announce" +# Whether CI should include auto-generated code to build local artifacts +build-local-artifacts = false +# Local artifacts jobs to run in CI +local-artifacts-jobs = ["./build-binaries", "./build-docker"] +# Publish jobs to run in CI +publish-jobs = ["./publish-pypi"] +# Post-announce jobs to run in CI +post-announce-jobs = ["./publish-docs"] +# Custom permissions for GitHub Jobs +github-custom-job-permissions = { "build-docker" = { packages = "write", contents = "read", id-token = "write", attestations = "write" } } +# Whether to install an updater program +install-updater = false +# Path that installers should place binaries in +install-path = [ + "$XDG_BIN_HOME/", + "$XDG_DATA_HOME/../bin", + "~/.local/bin" +] + +[dist.github-custom-runners] +global = "depot-ubuntu-latest-4" + +[dist.min-glibc-version] +# Override glibc version for specific target triplets. +aarch64-unknown-linux-gnu = "2.28" +riscv64gc-unknown-linux-gnu = "2.31" +# Override all remaining glibc versions. +"*" = "2.17" + +[dist.github-action-commits] +"actions/checkout" = "11bd71901bbe5b1630ceea73d27597364c9af683" # v4 +"actions/upload-artifact" = "6027e3dd177782cd8ab9af838c04fd81a07f1d47" # v4.6.2 +"actions/download-artifact" = "d3f86a106a0bac45b974a628896c90dbdf5c8093" # v4.3.0 +"actions/attest-build-provenance" = "c074443f1aee8d4aeeae555aebba3282517141b2" #v2.2.3 + +[dist.binaries] +"*" = ["uv", "uvx"] +# Add "uvw" binary for Windows targets +aarch64-pc-windows-msvc = ["uv", "uvx", "uvw"] +i686-pc-windows-msvc = ["uv", "uvx", "uvw"] +x86_64-pc-windows-msvc = ["uv", "uvx", "uvw"] From 563e9495ba015a4938e2a6330fcbd1c83e3fa2cc Mon Sep 17 00:00:00 2001 From: Lucas Vittor Date: Fri, 20 Jun 2025 17:05:17 -0300 Subject: [PATCH 054/185] Replace cuda124 with cuda128 (#14168) ## Summary Replace wrong `cuda124` version to the correct `cuda128` version in torch docs ## Test Plan --- docs/guides/integration/pytorch.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/guides/integration/pytorch.md b/docs/guides/integration/pytorch.md index fb1d82b31..7a2500ec5 100644 --- a/docs/guides/integration/pytorch.md +++ b/docs/guides/integration/pytorch.md @@ -355,11 +355,11 @@ explicit = true In some cases, you may want to use CPU-only builds in some cases, but CUDA-enabled builds in others, with the choice toggled by a user-provided extra (e.g., `uv sync --extra cpu` vs. -`uv sync --extra cu124`). +`uv sync --extra cu128`). With `tool.uv.sources`, you can use extra markers to specify the desired index for each enabled extra. For example, the following configuration would use PyTorch's CPU-only for -`uv sync --extra cpu` and CUDA-enabled builds for `uv sync --extra cu124`: +`uv sync --extra cpu` and CUDA-enabled builds for `uv sync --extra cu128`: ```toml [project] @@ -410,7 +410,7 @@ explicit = true !!! note Since GPU-accelerated builds aren't available on macOS, the above configuration will fail to install - on macOS when the `cu124` extra is enabled. + on macOS when the `cu128` extra is enabled. ## The `uv pip` interface From 1dbe7504528f7734b7721f54c8e90c0c691e786b Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Fri, 20 Jun 2025 15:31:32 -0500 Subject: [PATCH 055/185] Refactor `PythonVersionFile` global loading (#14107) I was looking into `uv tool` not supporting version files, and noticed this implementation was confusing and skipped handling like a tracing log if `--no-config` excludes selection a file. I've refactored it in preparation for the next change. --- crates/uv-python/src/version_files.rs | 76 ++++++++++++++------------- crates/uv/src/commands/python/pin.rs | 17 +++--- 2 files changed, 46 insertions(+), 47 deletions(-) diff --git a/crates/uv-python/src/version_files.rs b/crates/uv-python/src/version_files.rs index 894654a3c..a9cd05b7e 100644 --- a/crates/uv-python/src/version_files.rs +++ b/crates/uv-python/src/version_files.rs @@ -37,11 +37,14 @@ pub enum FilePreference { pub struct DiscoveryOptions<'a> { /// The path to stop discovery at. stop_discovery_at: Option<&'a Path>, - /// When `no_config` is set, Python version files will be ignored. + /// Ignore Python version files. /// /// Discovery will still run in order to display a log about the ignored file. no_config: bool, + /// Whether `.python-version` or `.python-versions` should be preferred. preference: FilePreference, + /// Whether to ignore local version files, and only search for a global one. + no_local: bool, } impl<'a> DiscoveryOptions<'a> { @@ -62,6 +65,11 @@ impl<'a> DiscoveryOptions<'a> { ..self } } + + #[must_use] + pub fn with_no_local(self, no_local: bool) -> Self { + Self { no_local, ..self } + } } impl PythonVersionFile { @@ -70,33 +78,38 @@ impl PythonVersionFile { working_directory: impl AsRef, options: &DiscoveryOptions<'_>, ) -> Result, std::io::Error> { - let Some(path) = Self::find_nearest(&working_directory, options) else { - if let Some(stop_discovery_at) = options.stop_discovery_at { - if stop_discovery_at == working_directory.as_ref() { - debug!( - "No Python version file found in workspace: {}", - working_directory.as_ref().display() - ); + let allow_local = !options.no_local; + let Some(path) = allow_local.then(|| { + // First, try to find a local version file. + let local = Self::find_nearest(&working_directory, options); + if local.is_none() { + // Log where we searched for the file, if not found + if let Some(stop_discovery_at) = options.stop_discovery_at { + if stop_discovery_at == working_directory.as_ref() { + debug!( + "No Python version file found in workspace: {}", + working_directory.as_ref().display() + ); + } else { + debug!( + "No Python version file found between working directory `{}` and workspace root `{}`", + working_directory.as_ref().display(), + stop_discovery_at.display() + ); + } } else { debug!( - "No Python version file found between working directory `{}` and workspace root `{}`", - working_directory.as_ref().display(), - stop_discovery_at.display() + "No Python version file found in ancestors of working directory: {}", + working_directory.as_ref().display() ); } - } else { - debug!( - "No Python version file found in ancestors of working directory: {}", - working_directory.as_ref().display() - ); } - // Not found in directory or its ancestors. Looking in user-level config. - return Ok(match user_uv_config_dir() { - Some(user_dir) => Self::discover_user_config(user_dir, options) - .await? - .or(None), - None => None, - }); + local + }).flatten().or_else(|| { + // Search for a global config + Self::find_global(options) + }) else { + return Ok(None); }; if options.no_config { @@ -111,20 +124,9 @@ impl PythonVersionFile { Self::try_from_path(path).await } - pub async fn discover_user_config( - user_config_working_directory: impl AsRef, - options: &DiscoveryOptions<'_>, - ) -> Result, std::io::Error> { - if !options.no_config { - if let Some(path) = - Self::find_in_directory(user_config_working_directory.as_ref(), options) - .into_iter() - .find(|path| path.is_file()) - { - return Self::try_from_path(path).await; - } - } - Ok(None) + fn find_global(options: &DiscoveryOptions<'_>) -> Option { + let user_config_dir = user_uv_config_dir()?; + Self::find_in_directory(&user_config_dir, options) } fn find_nearest(path: impl AsRef, options: &DiscoveryOptions<'_>) -> Option { diff --git a/crates/uv/src/commands/python/pin.rs b/crates/uv/src/commands/python/pin.rs index 26714e4d7..f0dc06cff 100644 --- a/crates/uv/src/commands/python/pin.rs +++ b/crates/uv/src/commands/python/pin.rs @@ -57,16 +57,13 @@ pub(crate) async fn pin( } }; - let version_file = if global { - if let Some(path) = user_uv_config_dir() { - PythonVersionFile::discover_user_config(path, &VersionFileDiscoveryOptions::default()) - .await - } else { - Ok(None) - } - } else { - PythonVersionFile::discover(project_dir, &VersionFileDiscoveryOptions::default()).await - }; + // Search for an existing file, we won't necessarily write to this, we'll construct a target + // path if there's a request later on. + let version_file = PythonVersionFile::discover( + project_dir, + &VersionFileDiscoveryOptions::default().with_no_local(global), + ) + .await; if rm { let Some(file) = version_file? else { From 0133bcc8ca36e47eaf42946a157b8dbb1f3b1be8 Mon Sep 17 00:00:00 2001 From: Jack O'Connor Date: Fri, 20 Jun 2025 13:34:45 -0700 Subject: [PATCH 056/185] (f)lock during `uv run` (#14153) This is very similar to the locking added for `uv sync`, `uv add`, and `uv remove` in https://github.com/astral-sh/uv/pull/13869. Improving our (f)locking in general is tracked in https://github.com/astral-sh/uv/issues/13883. --- crates/uv/src/commands/project/run.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index 8a510fa8c..ee3caeafa 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -240,6 +240,8 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl .await? .into_environment()?; + let _lock = environment.lock().await?; + // Determine the lock mode. let mode = if frozen { LockMode::Frozen @@ -382,6 +384,8 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl ) }); + let _lock = environment.lock().await?; + match update_environment( environment, spec, @@ -694,6 +698,8 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl .map(|lock| (lock, project.workspace().install_path().to_owned())); } } else { + let _lock = venv.lock().await?; + // Determine the lock mode. let mode = if frozen { LockMode::Frozen From e59835d50c8181af40badf03e8ef9d4152d25d65 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 20 Jun 2025 20:33:20 -0400 Subject: [PATCH 057/185] Add XPU to `--torch-backend` (#14172) ## Summary Like ROCm, no auto-detection for now. --- clippy.toml | 1 + crates/uv-resolver/src/resolver/system.rs | 6 ++++++ crates/uv-torch/src/backend.rs | 10 ++++++++++ docs/reference/cli.md | 3 +++ uv.schema.json | 7 +++++++ 5 files changed, 27 insertions(+) diff --git a/clippy.toml b/clippy.toml index bb1f365b5..6b3031c84 100644 --- a/clippy.toml +++ b/clippy.toml @@ -7,6 +7,7 @@ doc-valid-idents = [ "ReFS", "PyTorch", "ROCm", + "XPU", ".." # Include the defaults ] diff --git a/crates/uv-resolver/src/resolver/system.rs b/crates/uv-resolver/src/resolver/system.rs index a47000846..a815697da 100644 --- a/crates/uv-resolver/src/resolver/system.rs +++ b/crates/uv-resolver/src/resolver/system.rs @@ -86,4 +86,10 @@ mod tests { let url = DisplaySafeUrl::parse("https://download.pytorch.org/whl/cpu").unwrap(); assert_eq!(SystemDependency::from_index(&url), None); } + + #[test] + fn pytorch_xpu() { + let url = DisplaySafeUrl::parse("https://download.pytorch.org/whl/xpu").unwrap(); + assert_eq!(SystemDependency::from_index(&url), None); + } } diff --git a/crates/uv-torch/src/backend.rs b/crates/uv-torch/src/backend.rs index 958ea3d9e..60d43f3d7 100644 --- a/crates/uv-torch/src/backend.rs +++ b/crates/uv-torch/src/backend.rs @@ -171,6 +171,8 @@ pub enum TorchMode { #[serde(rename = "rocm4.0.1")] #[cfg_attr(feature = "clap", clap(name = "rocm4.0.1"))] Rocm401, + /// Use the PyTorch index for Intel XPU. + Xpu, } /// The strategy to use when determining the appropriate PyTorch index. @@ -237,6 +239,7 @@ impl TorchStrategy { TorchMode::Rocm42 => Ok(Self::Backend(TorchBackend::Rocm42)), TorchMode::Rocm41 => Ok(Self::Backend(TorchBackend::Rocm41)), TorchMode::Rocm401 => Ok(Self::Backend(TorchBackend::Rocm401)), + TorchMode::Xpu => Ok(Self::Backend(TorchBackend::Xpu)), } } @@ -356,6 +359,7 @@ pub enum TorchBackend { Rocm42, Rocm41, Rocm401, + Xpu, } impl TorchBackend { @@ -403,6 +407,7 @@ impl TorchBackend { Self::Rocm42 => &ROCM42_INDEX_URL, Self::Rocm41 => &ROCM41_INDEX_URL, Self::Rocm401 => &ROCM401_INDEX_URL, + Self::Xpu => &XPU_INDEX_URL, } } @@ -465,6 +470,7 @@ impl TorchBackend { TorchBackend::Rocm42 => None, TorchBackend::Rocm41 => None, TorchBackend::Rocm401 => None, + TorchBackend::Xpu => None, } } @@ -512,6 +518,7 @@ impl TorchBackend { TorchBackend::Rocm42 => Some(Version::new([4, 2])), TorchBackend::Rocm41 => Some(Version::new([4, 1])), TorchBackend::Rocm401 => Some(Version::new([4, 0, 1])), + TorchBackend::Xpu => None, } } } @@ -562,6 +569,7 @@ impl FromStr for TorchBackend { "rocm4.2" => Ok(TorchBackend::Rocm42), "rocm4.1" => Ok(TorchBackend::Rocm41), "rocm4.0.1" => Ok(TorchBackend::Rocm401), + "xpu" => Ok(TorchBackend::Xpu), _ => Err(format!("Unknown PyTorch backend: {s}")), } } @@ -725,3 +733,5 @@ static ROCM41_INDEX_URL: LazyLock = LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/rocm4.1").unwrap()); static ROCM401_INDEX_URL: LazyLock = LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/rocm4.0.1").unwrap()); +static XPU_INDEX_URL: LazyLock = + LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/xpu").unwrap()); diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 48b0351fe..b9490e2ce 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -3451,6 +3451,7 @@ by --python-version.

  • rocm4.2: Use the PyTorch index for ROCm 4.2
  • rocm4.1: Use the PyTorch index for ROCm 4.1
  • rocm4.0.1: Use the PyTorch index for ROCm 4.0.1
  • +
  • xpu: Use the PyTorch index for Intel XPU
  • --universal

    Perform a universal resolution, attempting to generate a single requirements.txt output file that is compatible with all operating systems, architectures, and Python implementations.

    In universal mode, the current Python version (or user-provided --python-version) will be treated as a lower bound. For example, --universal --python-version 3.7 would produce a universal resolution for Python 3.7 and later.

    Implies --no-strip-markers.

    @@ -3708,6 +3709,7 @@ be used with caution, as it can modify the system Python installation.

  • rocm4.2: Use the PyTorch index for ROCm 4.2
  • rocm4.1: Use the PyTorch index for ROCm 4.1
  • rocm4.0.1: Use the PyTorch index for ROCm 4.0.1
  • +
  • xpu: Use the PyTorch index for Intel XPU
  • --verbose, -v

    Use verbose output.

    You can configure fine-grained logging using the RUST_LOG environment variable. (https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives)

    @@ -3998,6 +4000,7 @@ should be used with caution, as it can modify the system Python installation.

    rocm4.2: Use the PyTorch index for ROCm 4.2
  • rocm4.1: Use the PyTorch index for ROCm 4.1
  • rocm4.0.1: Use the PyTorch index for ROCm 4.0.1
  • +
  • xpu: Use the PyTorch index for Intel XPU
  • --upgrade, -U

    Allow package upgrades, ignoring pinned versions in any existing output file. Implies --refresh

    --upgrade-package, -P upgrade-package

    Allow upgrades for a specific package, ignoring pinned versions in any existing output file. Implies --refresh-package

    --user
    --verbose, -v

    Use verbose output.

    diff --git a/uv.schema.json b/uv.schema.json index 0d2b47490..0b9bd6f15 100644 --- a/uv.schema.json +++ b/uv.schema.json @@ -2701,6 +2701,13 @@ "enum": [ "rocm4.0.1" ] + }, + { + "description": "Use the PyTorch index for Intel XPU.", + "type": "string", + "enum": [ + "xpu" + ] } ] }, From 0fef253c4b40c7721d16e52aed86f0ee24d6fa41 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 20 Jun 2025 20:33:29 -0400 Subject: [PATCH 058/185] Use a dedicated type for form metadata (#14175) --- crates/uv-publish/src/lib.rs | 227 +++++++++++++++++++---------------- 1 file changed, 122 insertions(+), 105 deletions(-) diff --git a/crates/uv-publish/src/lib.rs b/crates/uv-publish/src/lib.rs index ec19713cc..51f9bb472 100644 --- a/crates/uv-publish/src/lib.rs +++ b/crates/uv-publish/src/lib.rs @@ -390,7 +390,7 @@ pub async fn upload( download_concurrency: &Semaphore, reporter: Arc, ) -> Result { - let form_metadata = form_metadata(file, filename) + let form_metadata = FormMetadata::read_from_file(file, filename) .await .map_err(|err| PublishError::PublishPrepare(file.to_path_buf(), Box::new(err)))?; @@ -644,108 +644,118 @@ async fn metadata(file: &Path, filename: &DistFilename) -> Result -async fn form_metadata( - file: &Path, - filename: &DistFilename, -) -> Result, PublishPrepareError> { - let hash_hex = hash_file(file, Hasher::from(HashAlgorithm::Sha256)).await?; +#[derive(Debug, Clone)] +struct FormMetadata(Vec<(&'static str, String)>); - let Metadata23 { - metadata_version, - name, - version, - platforms, - // Not used by PyPI legacy upload - supported_platforms: _, - summary, - description, - description_content_type, - keywords, - home_page, - download_url, - author, - author_email, - maintainer, - maintainer_email, - license, - license_expression, - license_files, - classifiers, - requires_dist, - provides_dist, - obsoletes_dist, - requires_python, - requires_external, - project_urls, - provides_extras, - dynamic, - } = metadata(file, filename).await?; +impl FormMetadata { + /// Collect the non-file fields for the multipart request from the package METADATA. + /// + /// Reference implementation: + async fn read_from_file( + file: &Path, + filename: &DistFilename, + ) -> Result { + let hash_hex = hash_file(file, Hasher::from(HashAlgorithm::Sha256)).await?; - let mut form_metadata = vec![ - (":action", "file_upload".to_string()), - ("sha256_digest", hash_hex.digest.to_string()), - ("protocol_version", "1".to_string()), - ("metadata_version", metadata_version.clone()), - // Twine transforms the name with `re.sub("[^A-Za-z0-9.]+", "-", name)` - // * - // * - // warehouse seems to call `packaging.utils.canonicalize_name` nowadays and has a separate - // `normalized_name`, so we'll start with this and we'll readjust if there are user reports. - ("name", name.clone()), - ("version", version.clone()), - ("filetype", filename.filetype().to_string()), - ]; + let Metadata23 { + metadata_version, + name, + version, + platforms, + // Not used by PyPI legacy upload + supported_platforms: _, + summary, + description, + description_content_type, + keywords, + home_page, + download_url, + author, + author_email, + maintainer, + maintainer_email, + license, + license_expression, + license_files, + classifiers, + requires_dist, + provides_dist, + obsoletes_dist, + requires_python, + requires_external, + project_urls, + provides_extras, + dynamic, + } = metadata(file, filename).await?; - if let DistFilename::WheelFilename(wheel) = filename { - form_metadata.push(("pyversion", wheel.python_tags().iter().join("."))); - } else { - form_metadata.push(("pyversion", "source".to_string())); + let mut form_metadata = vec![ + (":action", "file_upload".to_string()), + ("sha256_digest", hash_hex.digest.to_string()), + ("protocol_version", "1".to_string()), + ("metadata_version", metadata_version.clone()), + // Twine transforms the name with `re.sub("[^A-Za-z0-9.]+", "-", name)` + // * + // * + // warehouse seems to call `packaging.utils.canonicalize_name` nowadays and has a separate + // `normalized_name`, so we'll start with this and we'll readjust if there are user reports. + ("name", name.clone()), + ("version", version.clone()), + ("filetype", filename.filetype().to_string()), + ]; + + if let DistFilename::WheelFilename(wheel) = filename { + form_metadata.push(("pyversion", wheel.python_tags().iter().join("."))); + } else { + form_metadata.push(("pyversion", "source".to_string())); + } + + let mut add_option = |name, value: Option| { + if let Some(some) = value.clone() { + form_metadata.push((name, some)); + } + }; + + add_option("author", author); + add_option("author_email", author_email); + add_option("description", description); + add_option("description_content_type", description_content_type); + add_option("download_url", download_url); + add_option("home_page", home_page); + add_option("keywords", keywords); + add_option("license", license); + add_option("license_expression", license_expression); + add_option("maintainer", maintainer); + add_option("maintainer_email", maintainer_email); + add_option("summary", summary); + + // The GitLab PyPI repository API implementation requires this metadata field and twine always + // includes it in the request, even when it's empty. + form_metadata.push(("requires_python", requires_python.unwrap_or(String::new()))); + + let mut add_vec = |name, values: Vec| { + for i in values { + form_metadata.push((name, i.clone())); + } + }; + + add_vec("classifiers", classifiers); + add_vec("dynamic", dynamic); + add_vec("license_file", license_files); + add_vec("obsoletes_dist", obsoletes_dist); + add_vec("platform", platforms); + add_vec("project_urls", project_urls); + add_vec("provides_dist", provides_dist); + add_vec("provides_extra", provides_extras); + add_vec("requires_dist", requires_dist); + add_vec("requires_external", requires_external); + + Ok(Self(form_metadata)) } - let mut add_option = |name, value: Option| { - if let Some(some) = value.clone() { - form_metadata.push((name, some)); - } - }; - - add_option("author", author); - add_option("author_email", author_email); - add_option("description", description); - add_option("description_content_type", description_content_type); - add_option("download_url", download_url); - add_option("home_page", home_page); - add_option("keywords", keywords); - add_option("license", license); - add_option("license_expression", license_expression); - add_option("maintainer", maintainer); - add_option("maintainer_email", maintainer_email); - add_option("summary", summary); - - // The GitLab PyPI repository API implementation requires this metadata field and twine always - // includes it in the request, even when it's empty. - form_metadata.push(("requires_python", requires_python.unwrap_or(String::new()))); - - let mut add_vec = |name, values: Vec| { - for i in values { - form_metadata.push((name, i.clone())); - } - }; - - add_vec("classifiers", classifiers); - add_vec("dynamic", dynamic); - add_vec("license_file", license_files); - add_vec("obsoletes_dist", obsoletes_dist); - add_vec("platform", platforms); - add_vec("project_urls", project_urls); - add_vec("provides_dist", provides_dist); - add_vec("provides_extra", provides_extras); - add_vec("requires_dist", requires_dist); - add_vec("requires_external", requires_external); - - Ok(form_metadata) + /// Returns an iterator over the metadata fields. + fn iter(&self) -> std::slice::Iter<'_, (&'static str, String)> { + self.0.iter() + } } /// Build the upload request. @@ -758,11 +768,11 @@ async fn build_request<'a>( registry: &DisplaySafeUrl, client: &'a BaseClient, credentials: &Credentials, - form_metadata: &[(&'static str, String)], + form_metadata: &FormMetadata, reporter: Arc, ) -> Result<(RequestBuilder<'a>, usize), PublishPrepareError> { let mut form = reqwest::multipart::Form::new(); - for (key, value) in form_metadata { + for (key, value) in form_metadata.iter() { form = form.text(*key, value.clone()); } @@ -888,16 +898,19 @@ async fn handle_response(registry: &Url, response: Response) -> Result<(), Publi #[cfg(test)] mod tests { - use crate::{Reporter, build_request, form_metadata}; - use insta::{assert_debug_snapshot, assert_snapshot}; - use itertools::Itertools; use std::path::PathBuf; use std::sync::Arc; + + use insta::{assert_debug_snapshot, assert_snapshot}; + use itertools::Itertools; + use uv_auth::Credentials; use uv_client::BaseClientBuilder; use uv_distribution_filename::DistFilename; use uv_redacted::DisplaySafeUrl; + use crate::{FormMetadata, Reporter, build_request}; + struct DummyReporter; impl Reporter for DummyReporter { @@ -916,7 +929,9 @@ mod tests { let file = PathBuf::from("../../scripts/links/").join(raw_filename); let filename = DistFilename::try_from_normalized_filename(raw_filename).unwrap(); - let form_metadata = form_metadata(&file, &filename).await.unwrap(); + let form_metadata = FormMetadata::read_from_file(&file, &filename) + .await + .unwrap(); let formatted_metadata = form_metadata .iter() @@ -1028,7 +1043,9 @@ mod tests { let file = PathBuf::from("../../scripts/links/").join(raw_filename); let filename = DistFilename::try_from_normalized_filename(raw_filename).unwrap(); - let form_metadata = form_metadata(&file, &filename).await.unwrap(); + let form_metadata = FormMetadata::read_from_file(&file, &filename) + .await + .unwrap(); let formatted_metadata = form_metadata .iter() From 8352560b98bdfd830b842cb4e2cc0aa1a8e50357 Mon Sep 17 00:00:00 2001 From: John Mumm Date: Sat, 21 Jun 2025 03:50:41 -0400 Subject: [PATCH 059/185] Only update existing symlink directories on preview uninstall (#14179) On preview uninstall, we should not create a new minor version symlink directory if one doesn't exist. We should only update existing ones. --- crates/uv/src/commands/python/uninstall.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/uv/src/commands/python/uninstall.rs b/crates/uv/src/commands/python/uninstall.rs index 8a63b015c..e79ea9b19 100644 --- a/crates/uv/src/commands/python/uninstall.rs +++ b/crates/uv/src/commands/python/uninstall.rs @@ -240,7 +240,7 @@ async fn do_uninstall( .iter() .filter(|(minor_version, _)| uninstalled_minor_versions.contains(minor_version)) { - installation.ensure_minor_version_link(preview)?; + installation.update_minor_version_link(preview)?; } // For each uninstalled installation, check if there are no remaining installations // for its minor version. If there are none remaining, remove the symlink directory From b18f45db14152e75b85351afd69558e41b674325 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Sat, 21 Jun 2025 09:01:20 -0500 Subject: [PATCH 060/185] Restore Docker image annotations and fix attestations (#14165) More follow-up to #13459 - Depot doesn't support annotations, so we push those manually - Docker push for the re-tag was breaking the manifest, since we need to annotate manually, we just do that instead - We attest after the annotation A bit of an aside - We test building the extra images, it's very fast and I don't see why it's better to gate it I tested this on my fork then cleaned it up a bit for a commit here. You can see the images at - https://github.com/zanieb/uv/pkgs/container/uv - https://hub.docker.com/r/astral/uv/tags --------- Co-authored-by: samypr100 <3933065+samypr100@users.noreply.github.com> --- .github/workflows/build-docker.yml | 126 +++++++++++++++++++++++++---- 1 file changed, 110 insertions(+), 16 deletions(-) diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml index 1f5229aef..1ac48ee0f 100644 --- a/.github/workflows/build-docker.yml +++ b/.github/workflows/build-docker.yml @@ -80,7 +80,9 @@ jobs: name: release outputs: image-tags: ${{ steps.meta.outputs.tags }} + image-annotations: ${{ steps.meta.outputs.annotations }} image-digest: ${{ steps.build.outputs.digest }} + image-version: ${{ steps.meta.outputs.version }} steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: @@ -117,6 +119,8 @@ jobs: - name: Extract metadata (tags, labels) for Docker id: meta uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0 + env: + DOCKER_METADATA_ANNOTATIONS_LEVELS: index with: images: | ${{ env.UV_GHCR_IMAGE }} @@ -137,10 +141,12 @@ jobs: push: ${{ needs.docker-plan.outputs.push }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} + # TODO(zanieb): Annotations are not supported by Depot yet and are ignored + annotations: ${{ steps.meta.outputs.annotations }} - name: Generate artifact attestation for base image if: ${{ needs.docker-plan.outputs.push == 'true' }} - uses: actions/attest-build-provenance@c074443f1aee8d4aeeae555aebba3282517141b2 # v2.2.3 + uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0 with: subject-name: ${{ env.UV_GHCR_IMAGE }} subject-digest: ${{ steps.build.outputs.digest }} @@ -153,7 +159,6 @@ jobs: needs: - docker-plan - docker-publish-base - if: ${{ needs.docker-plan.outputs.push == 'true' }} permissions: id-token: write # for Depot OIDC and GHCR signing packages: write # for GHCR image pushes @@ -263,20 +268,66 @@ jobs: context: . project: 7hd4vdzmw5 # astral-sh/uv platforms: linux/amd64,linux/arm64 - push: true + push: ${{ needs.docker-plan.outputs.push }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} + # TODO(zanieb): Annotations are not supported by Depot yet and are ignored annotations: ${{ steps.meta.outputs.annotations }} - name: Generate artifact attestation + if: ${{ needs.docker-plan.outputs.push == 'true' }} uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0 with: subject-name: ${{ env.UV_GHCR_IMAGE }} subject-digest: ${{ steps.build-and-push.outputs.digest }} - # Re-tag the base image, to ensure it's shown as the newest on the registry UI - docker-retag-base: - name: retag uv + # Push annotations manually. + # See `docker-annotate-base` for details. + - name: Add annotations to images + if: ${{ needs.docker-plan.outputs.push == 'true' }} + env: + IMAGES: "${{ env.UV_GHCR_IMAGE }} ${{ env.UV_DOCKERHUB_IMAGE }}" + DIGEST: ${{ steps.build-and-push.outputs.digest }} + TAGS: ${{ steps.meta.outputs.tags }} + ANNOTATIONS: ${{ steps.meta.outputs.annotations }} + run: | + set -x + readarray -t lines <<< "$ANNOTATIONS"; annotations=(); for line in "${lines[@]}"; do annotations+=(--annotation "$line"); done + for image in $IMAGES; do + readarray -t lines < <(grep "^${image}:" <<< "$TAGS"); tags=(); for line in "${lines[@]}"; do tags+=(-t "$line"); done + docker buildx imagetools create \ + "${annotations[@]}" \ + "${tags[@]}" \ + "${image}@${DIGEST}" + done + + # See `docker-annotate-base` for details. + - name: Export manifest digest + id: manifest-digest + if: ${{ needs.docker-plan.outputs.push == 'true' }} + env: + IMAGE: ${{ env.UV_GHCR_IMAGE }} + VERSION: ${{ steps.meta.outputs.version }} + run: | + digest="$( + docker buildx imagetools inspect \ + "${IMAGE}:${VERSION}" \ + --format '{{json .Manifest}}' \ + | jq -r '.digest' + )" + echo "digest=${digest}" >> "$GITHUB_OUTPUT" + + # See `docker-annotate-base` for details. + - name: Generate artifact attestation + if: ${{ needs.docker-plan.outputs.push == 'true' }} + uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0 + with: + subject-name: ${{ env.UV_GHCR_IMAGE }} + subject-digest: ${{ steps.manifest-digest.outputs.digest }} + + # Annotate the base image + docker-annotate-base: + name: annotate uv runs-on: ubuntu-latest environment: name: release @@ -286,24 +337,67 @@ jobs: - docker-publish-extra if: ${{ needs.docker-plan.outputs.push == 'true' }} steps: + - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 + with: + username: astral + password: ${{ secrets.DOCKERHUB_TOKEN_RW }} + - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Push tags + # Depot doesn't support annotating images, so we need to do so manually + # afterwards. Mutating the manifest is desirable regardless, because we + # want to bump the base image to appear at the top of the list on GHCR. + # However, once annotation support is added to Depot, this step can be + # minimized to just touch the GHCR manifest. + - name: Add annotations to images env: - IMAGE: ${{ env.UV_GHCR_IMAGE }} + IMAGES: "${{ env.UV_GHCR_IMAGE }} ${{ env.UV_DOCKERHUB_IMAGE }}" DIGEST: ${{ needs.docker-publish-base.outputs.image-digest }} TAGS: ${{ needs.docker-publish-base.outputs.image-tags }} + ANNOTATIONS: ${{ needs.docker-publish-base.outputs.image-annotations }} + # The readarray part is used to make sure the quoting and special characters are preserved on expansion (e.g. spaces) + # The final command becomes `docker buildx imagetools create --annotation 'index:foo=1' --annotation 'index:bar=2' ... -t tag1 -t tag2 ... @sha256:` run: | - docker pull "${IMAGE}@${DIGEST}" - for tag in $TAGS; do - # Skip re-tag for DockerHub - if [[ "$tag" == "${{ env.UV_DOCKERHUB_IMAGE }}"* ]]; then - continue - fi - docker tag "${IMAGE}@${DIGEST}" "${tag}" - docker push "${tag}" + set -x + readarray -t lines <<< "$ANNOTATIONS"; annotations=(); for line in "${lines[@]}"; do annotations+=(--annotation "$line"); done + for image in $IMAGES; do + readarray -t lines < <(grep "^${image}:" <<< "$TAGS"); tags=(); for line in "${lines[@]}"; do tags+=(-t "$line"); done + docker buildx imagetools create \ + "${annotations[@]}" \ + "${tags[@]}" \ + "${image}@${DIGEST}" done + + # Now that we've modified the manifest, we need to attest it again. + # Note we only generate an attestation for GHCR. + - name: Export manifest digest + id: manifest-digest + env: + IMAGE: ${{ env.UV_GHCR_IMAGE }} + VERSION: ${{ needs.docker-publish-base.outputs.image-version }} + # To sign the manifest, we need it's digest. Unfortunately "docker + # buildx imagetools create" does not (yet) have a clean way of sharing + # the digest of the manifest it creates (see docker/buildx#2407), so + # we use a separate command to retrieve it. + # imagetools inspect [TAG] --format '{{json .Manifest}}' gives us + # the machine readable JSON description of the manifest, and the + # jq command extracts the digest from this. The digest is then + # sent to the Github step output file for sharing with other steps. + run: | + digest="$( + docker buildx imagetools inspect \ + "${IMAGE}:${VERSION}" \ + --format '{{json .Manifest}}' \ + | jq -r '.digest' + )" + echo "digest=${digest}" >> "$GITHUB_OUTPUT" + + - name: Generate artifact attestation + uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0 + with: + subject-name: ${{ env.UV_GHCR_IMAGE }} + subject-digest: ${{ steps.manifest-digest.outputs.digest }} From f0407e4b6f29ea053efb7f8eec12435d54f05f76 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Sat, 21 Jun 2025 09:36:07 -0500 Subject: [PATCH 061/185] Only use the `release` environment in Docker builds on push (#14189) This drastically reduces the `release` deployment noise, e.g., as seen in https://github.com/astral-sh/uv/pull/14165 --- .github/workflows/build-docker.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml index 1ac48ee0f..25e042275 100644 --- a/.github/workflows/build-docker.yml +++ b/.github/workflows/build-docker.yml @@ -77,7 +77,7 @@ jobs: packages: write # for GHCR image pushes attestations: write # for GHCR attestations environment: - name: release + name: ${{ needs.docker-plan.outputs.push == 'true' && 'release' || '' }} outputs: image-tags: ${{ steps.meta.outputs.tags }} image-annotations: ${{ steps.meta.outputs.annotations }} @@ -155,7 +155,7 @@ jobs: name: ${{ needs.docker-plan.outputs.action }} ${{ matrix.image-mapping }} runs-on: ubuntu-latest environment: - name: release + name: ${{ needs.docker-plan.outputs.push == 'true' && 'release' || '' }} needs: - docker-plan - docker-publish-base @@ -330,7 +330,7 @@ jobs: name: annotate uv runs-on: ubuntu-latest environment: - name: release + name: ${{ needs.docker-plan.outputs.push == 'true' && 'release' || '' }} needs: - docker-plan - docker-publish-base From a82c210cabde1bb4422639d93ae2791a827c0bc9 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 21 Jun 2025 11:21:06 -0400 Subject: [PATCH 062/185] Add auto-detection for AMD GPUs (#14176) ## Summary Allows `--torch-backend=auto` to detect AMD GPUs. The approach is fairly well-documented inline, but I opted for `rocm_agent_enumerator` over (e.g.) `rocminfo` since it seems to be the recommended approach for scripting: https://rocm.docs.amd.com/projects/rocminfo/en/latest/how-to/use-rocm-agent-enumerator.html. Closes https://github.com/astral-sh/uv/issues/14086. ## Test Plan ``` root@rocm-jupyter-gpu-mi300x1-192gb-devcloud-atl1:~# ./uv-linux-libc-11fb582c5c046bae09766ceddd276dcc5bb41218/uv pip install torch --torch-backend=auto Resolved 11 packages in 251ms Prepared 2 packages in 6ms Installed 11 packages in 257ms + filelock==3.18.0 + fsspec==2025.5.1 + jinja2==3.1.6 + markupsafe==3.0.2 + mpmath==1.3.0 + networkx==3.5 + pytorch-triton-rocm==3.3.1 + setuptools==80.9.0 + sympy==1.14.0 + torch==2.7.1+rocm6.3 + typing-extensions==4.14.0 ``` --------- Co-authored-by: Zanie Blue --- crates/uv-resolver/src/resolver/mod.rs | 2 +- crates/uv-static/src/env_vars.rs | 6 +- crates/uv-torch/src/accelerator.rs | 110 +++++++++++++++- crates/uv-torch/src/backend.rs | 168 ++++++++++++++++++++----- docs/guides/integration/pytorch.md | 11 +- 5 files changed, 257 insertions(+), 40 deletions(-) diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index 1384ce4f7..ed1cd48af 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -1814,7 +1814,7 @@ impl ResolverState std::fmt::Result { match self { Self::Cuda { driver_version } => write!(f, "CUDA {driver_version}"), + Self::Amd { gpu_architecture } => write!(f, "AMD {gpu_architecture}"), } } } @@ -33,9 +46,11 @@ impl Accelerator { /// /// Query, in order: /// 1. The `UV_CUDA_DRIVER_VERSION` environment variable. + /// 2. The `UV_AMD_GPU_ARCHITECTURE` environment variable. /// 2. `/sys/module/nvidia/version`, which contains the driver version (e.g., `550.144.03`). /// 3. `/proc/driver/nvidia/version`, which contains the driver version among other information. /// 4. `nvidia-smi --query-gpu=driver_version --format=csv,noheader`. + /// 5. `rocm_agent_enumerator`, which lists the AMD GPU architectures. pub fn detect() -> Result, AcceleratorError> { // Read from `UV_CUDA_DRIVER_VERSION`. if let Ok(driver_version) = std::env::var(EnvVars::UV_CUDA_DRIVER_VERSION) { @@ -44,6 +59,15 @@ impl Accelerator { return Ok(Some(Self::Cuda { driver_version })); } + // Read from `UV_AMD_GPU_ARCHITECTURE`. + if let Ok(gpu_architecture) = std::env::var(EnvVars::UV_AMD_GPU_ARCHITECTURE) { + let gpu_architecture = AmdGpuArchitecture::from_str(&gpu_architecture)?; + debug!( + "Detected AMD GPU architecture from `UV_AMD_GPU_ARCHITECTURE`: {gpu_architecture}" + ); + return Ok(Some(Self::Amd { gpu_architecture })); + } + // Read from `/sys/module/nvidia/version`. match fs_err::read_to_string("/sys/module/nvidia/version") { Ok(content) => { @@ -100,7 +124,34 @@ impl Accelerator { ); } - debug!("Failed to detect CUDA driver version"); + // Query `rocm_agent_enumerator` to detect the AMD GPU architecture. + // + // See: https://rocm.docs.amd.com/projects/rocminfo/en/latest/how-to/use-rocm-agent-enumerator.html + if let Ok(output) = std::process::Command::new("rocm_agent_enumerator").output() { + if output.status.success() { + let stdout = String::from_utf8(output.stdout)?; + if let Some(gpu_architecture) = stdout + .lines() + .map(str::trim) + .filter_map(|line| AmdGpuArchitecture::from_str(line).ok()) + .min() + { + debug!( + "Detected AMD GPU architecture from `rocm_agent_enumerator`: {gpu_architecture}" + ); + return Ok(Some(Self::Amd { gpu_architecture })); + } + } else { + debug!( + "Failed to query AMD GPU architecture with `rocm_agent_enumerator` with status `{}`: {}", + output.status, + String::from_utf8_lossy(&output.stderr) + ); + } + } + + debug!("Failed to detect GPU driver version"); + Ok(None) } } @@ -129,6 +180,63 @@ fn parse_proc_driver_nvidia_version(content: &str) -> Result, Ac Ok(Some(driver_version)) } +/// A GPU architecture for AMD GPUs. +/// +/// See: +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] +pub enum AmdGpuArchitecture { + Gfx900, + Gfx906, + Gfx908, + Gfx90a, + Gfx942, + Gfx1030, + Gfx1100, + Gfx1101, + Gfx1102, + Gfx1200, + Gfx1201, +} + +impl FromStr for AmdGpuArchitecture { + type Err = AcceleratorError; + + fn from_str(s: &str) -> Result { + match s { + "gfx900" => Ok(Self::Gfx900), + "gfx906" => Ok(Self::Gfx906), + "gfx908" => Ok(Self::Gfx908), + "gfx90a" => Ok(Self::Gfx90a), + "gfx942" => Ok(Self::Gfx942), + "gfx1030" => Ok(Self::Gfx1030), + "gfx1100" => Ok(Self::Gfx1100), + "gfx1101" => Ok(Self::Gfx1101), + "gfx1102" => Ok(Self::Gfx1102), + "gfx1200" => Ok(Self::Gfx1200), + "gfx1201" => Ok(Self::Gfx1201), + _ => Err(AcceleratorError::UnknownAmdGpuArchitecture(s.to_string())), + } + } +} + +impl std::fmt::Display for AmdGpuArchitecture { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Gfx900 => write!(f, "gfx900"), + Self::Gfx906 => write!(f, "gfx906"), + Self::Gfx908 => write!(f, "gfx908"), + Self::Gfx90a => write!(f, "gfx90a"), + Self::Gfx942 => write!(f, "gfx942"), + Self::Gfx1030 => write!(f, "gfx1030"), + Self::Gfx1100 => write!(f, "gfx1100"), + Self::Gfx1101 => write!(f, "gfx1101"), + Self::Gfx1102 => write!(f, "gfx1102"), + Self::Gfx1200 => write!(f, "gfx1200"), + Self::Gfx1201 => write!(f, "gfx1201"), + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/uv-torch/src/backend.rs b/crates/uv-torch/src/backend.rs index 60d43f3d7..0f2b72077 100644 --- a/crates/uv-torch/src/backend.rs +++ b/crates/uv-torch/src/backend.rs @@ -47,7 +47,7 @@ use uv_normalize::PackageName; use uv_pep440::Version; use uv_platform_tags::Os; -use crate::{Accelerator, AcceleratorError}; +use crate::{Accelerator, AcceleratorError, AmdGpuArchitecture}; /// The strategy to use when determining the appropriate PyTorch index. #[derive(Debug, Copy, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)] @@ -178,8 +178,13 @@ pub enum TorchMode { /// The strategy to use when determining the appropriate PyTorch index. #[derive(Debug, Clone, Eq, PartialEq)] pub enum TorchStrategy { - /// Select the appropriate PyTorch index based on the operating system and CUDA driver version. - Auto { os: Os, driver_version: Version }, + /// Select the appropriate PyTorch index based on the operating system and CUDA driver version (e.g., `550.144.03`). + Cuda { os: Os, driver_version: Version }, + /// Select the appropriate PyTorch index based on the operating system and AMD GPU architecture (e.g., `gfx1100`). + Amd { + os: Os, + gpu_architecture: AmdGpuArchitecture, + }, /// Use the specified PyTorch index. Backend(TorchBackend), } @@ -188,16 +193,17 @@ impl TorchStrategy { /// Determine the [`TorchStrategy`] from the given [`TorchMode`], [`Os`], and [`Accelerator`]. pub fn from_mode(mode: TorchMode, os: &Os) -> Result { match mode { - TorchMode::Auto => { - if let Some(Accelerator::Cuda { driver_version }) = Accelerator::detect()? { - Ok(Self::Auto { - os: os.clone(), - driver_version: driver_version.clone(), - }) - } else { - Ok(Self::Backend(TorchBackend::Cpu)) - } - } + TorchMode::Auto => match Accelerator::detect()? { + Some(Accelerator::Cuda { driver_version }) => Ok(Self::Cuda { + os: os.clone(), + driver_version: driver_version.clone(), + }), + Some(Accelerator::Amd { gpu_architecture }) => Ok(Self::Amd { + os: os.clone(), + gpu_architecture, + }), + None => Ok(Self::Backend(TorchBackend::Cpu)), + }, TorchMode::Cpu => Ok(Self::Backend(TorchBackend::Cpu)), TorchMode::Cu128 => Ok(Self::Backend(TorchBackend::Cu128)), TorchMode::Cu126 => Ok(Self::Backend(TorchBackend::Cu126)), @@ -267,25 +273,27 @@ impl TorchStrategy { /// Return the appropriate index URLs for the given [`TorchStrategy`]. pub fn index_urls(&self) -> impl Iterator { match self { - TorchStrategy::Auto { os, driver_version } => { + TorchStrategy::Cuda { os, driver_version } => { // If this is a GPU-enabled package, and CUDA drivers are installed, use PyTorch's CUDA // indexes. // // See: https://github.com/pmeier/light-the-torch/blob/33397cbe45d07b51ad8ee76b004571a4c236e37f/light_the_torch/_patch.py#L36-L49 match os { - Os::Manylinux { .. } | Os::Musllinux { .. } => Either::Left(Either::Left( - LINUX_DRIVERS - .iter() - .filter_map(move |(backend, version)| { - if driver_version >= version { - Some(backend.index_url()) - } else { - None - } - }) - .chain(std::iter::once(TorchBackend::Cpu.index_url())), - )), - Os::Windows => Either::Left(Either::Right( + Os::Manylinux { .. } | Os::Musllinux { .. } => { + Either::Left(Either::Left(Either::Left( + LINUX_CUDA_DRIVERS + .iter() + .filter_map(move |(backend, version)| { + if driver_version >= version { + Some(backend.index_url()) + } else { + None + } + }) + .chain(std::iter::once(TorchBackend::Cpu.index_url())), + ))) + } + Os::Windows => Either::Left(Either::Left(Either::Right( WINDOWS_CUDA_VERSIONS .iter() .filter_map(move |(backend, version)| { @@ -296,7 +304,7 @@ impl TorchStrategy { } }) .chain(std::iter::once(TorchBackend::Cpu.index_url())), - )), + ))), Os::Macos { .. } | Os::FreeBsd { .. } | Os::NetBsd { .. } @@ -306,11 +314,42 @@ impl TorchStrategy { | Os::Haiku { .. } | Os::Android { .. } | Os::Pyodide { .. } => { - Either::Right(std::iter::once(TorchBackend::Cpu.index_url())) + Either::Right(Either::Left(std::iter::once(TorchBackend::Cpu.index_url()))) } } } - TorchStrategy::Backend(backend) => Either::Right(std::iter::once(backend.index_url())), + TorchStrategy::Amd { + os, + gpu_architecture, + } => match os { + Os::Manylinux { .. } | Os::Musllinux { .. } => Either::Left(Either::Right( + LINUX_AMD_GPU_DRIVERS + .iter() + .filter_map(move |(backend, architecture)| { + if gpu_architecture == architecture { + Some(backend.index_url()) + } else { + None + } + }) + .chain(std::iter::once(TorchBackend::Cpu.index_url())), + )), + Os::Windows + | Os::Macos { .. } + | Os::FreeBsd { .. } + | Os::NetBsd { .. } + | Os::OpenBsd { .. } + | Os::Dragonfly { .. } + | Os::Illumos { .. } + | Os::Haiku { .. } + | Os::Android { .. } + | Os::Pyodide { .. } => { + Either::Right(Either::Left(std::iter::once(TorchBackend::Cpu.index_url()))) + } + }, + TorchStrategy::Backend(backend) => { + Either::Right(Either::Right(std::iter::once(backend.index_url()))) + } } } } @@ -578,7 +617,7 @@ impl FromStr for TorchBackend { /// Linux CUDA driver versions and the corresponding CUDA versions. /// /// See: -static LINUX_DRIVERS: LazyLock<[(TorchBackend, Version); 24]> = LazyLock::new(|| { +static LINUX_CUDA_DRIVERS: LazyLock<[(TorchBackend, Version); 24]> = LazyLock::new(|| { [ // Table 2 from // https://docs.nvidia.com/cuda/cuda-toolkit-release-notes/index.html @@ -651,6 +690,73 @@ static WINDOWS_CUDA_VERSIONS: LazyLock<[(TorchBackend, Version); 24]> = LazyLock ] }); +/// Linux AMD GPU architectures and the corresponding PyTorch backends. +/// +/// These were inferred by running the following snippet for each ROCm version: +/// +/// ```python +/// import torch +/// +/// print(torch.cuda.get_arch_list()) +/// ``` +/// +/// AMD also provides a compatibility matrix: ; +/// however, this list includes a broader array of GPUs than those in the matrix. +static LINUX_AMD_GPU_DRIVERS: LazyLock<[(TorchBackend, AmdGpuArchitecture); 44]> = + LazyLock::new(|| { + [ + // ROCm 6.3 + (TorchBackend::Rocm63, AmdGpuArchitecture::Gfx900), + (TorchBackend::Rocm63, AmdGpuArchitecture::Gfx906), + (TorchBackend::Rocm63, AmdGpuArchitecture::Gfx908), + (TorchBackend::Rocm63, AmdGpuArchitecture::Gfx90a), + (TorchBackend::Rocm63, AmdGpuArchitecture::Gfx942), + (TorchBackend::Rocm63, AmdGpuArchitecture::Gfx1030), + (TorchBackend::Rocm63, AmdGpuArchitecture::Gfx1100), + (TorchBackend::Rocm63, AmdGpuArchitecture::Gfx1101), + (TorchBackend::Rocm63, AmdGpuArchitecture::Gfx1102), + (TorchBackend::Rocm63, AmdGpuArchitecture::Gfx1200), + (TorchBackend::Rocm63, AmdGpuArchitecture::Gfx1201), + // ROCm 6.2.4 + (TorchBackend::Rocm624, AmdGpuArchitecture::Gfx900), + (TorchBackend::Rocm624, AmdGpuArchitecture::Gfx906), + (TorchBackend::Rocm624, AmdGpuArchitecture::Gfx908), + (TorchBackend::Rocm624, AmdGpuArchitecture::Gfx90a), + (TorchBackend::Rocm624, AmdGpuArchitecture::Gfx942), + (TorchBackend::Rocm624, AmdGpuArchitecture::Gfx1030), + (TorchBackend::Rocm624, AmdGpuArchitecture::Gfx1100), + (TorchBackend::Rocm624, AmdGpuArchitecture::Gfx1101), + (TorchBackend::Rocm624, AmdGpuArchitecture::Gfx1102), + (TorchBackend::Rocm624, AmdGpuArchitecture::Gfx1200), + (TorchBackend::Rocm624, AmdGpuArchitecture::Gfx1201), + // ROCm 6.2 + (TorchBackend::Rocm62, AmdGpuArchitecture::Gfx900), + (TorchBackend::Rocm62, AmdGpuArchitecture::Gfx906), + (TorchBackend::Rocm62, AmdGpuArchitecture::Gfx908), + (TorchBackend::Rocm62, AmdGpuArchitecture::Gfx90a), + (TorchBackend::Rocm62, AmdGpuArchitecture::Gfx1030), + (TorchBackend::Rocm62, AmdGpuArchitecture::Gfx1100), + (TorchBackend::Rocm62, AmdGpuArchitecture::Gfx942), + // ROCm 6.1 + (TorchBackend::Rocm61, AmdGpuArchitecture::Gfx900), + (TorchBackend::Rocm61, AmdGpuArchitecture::Gfx906), + (TorchBackend::Rocm61, AmdGpuArchitecture::Gfx908), + (TorchBackend::Rocm61, AmdGpuArchitecture::Gfx90a), + (TorchBackend::Rocm61, AmdGpuArchitecture::Gfx942), + (TorchBackend::Rocm61, AmdGpuArchitecture::Gfx1030), + (TorchBackend::Rocm61, AmdGpuArchitecture::Gfx1100), + (TorchBackend::Rocm61, AmdGpuArchitecture::Gfx1101), + // ROCm 6.0 + (TorchBackend::Rocm60, AmdGpuArchitecture::Gfx900), + (TorchBackend::Rocm60, AmdGpuArchitecture::Gfx906), + (TorchBackend::Rocm60, AmdGpuArchitecture::Gfx908), + (TorchBackend::Rocm60, AmdGpuArchitecture::Gfx90a), + (TorchBackend::Rocm60, AmdGpuArchitecture::Gfx1030), + (TorchBackend::Rocm60, AmdGpuArchitecture::Gfx1100), + (TorchBackend::Rocm60, AmdGpuArchitecture::Gfx942), + ] + }); + static CPU_INDEX_URL: LazyLock = LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/cpu").unwrap()); static CU128_INDEX_URL: LazyLock = diff --git a/docs/guides/integration/pytorch.md b/docs/guides/integration/pytorch.md index 7a2500ec5..a90ebeb6b 100644 --- a/docs/guides/integration/pytorch.md +++ b/docs/guides/integration/pytorch.md @@ -444,10 +444,10 @@ $ # With an environment variable. $ UV_TORCH_BACKEND=auto uv pip install torch ``` -When enabled, uv will query for the installed CUDA driver version and use the most-compatible -PyTorch index for all relevant packages (e.g., `torch`, `torchvision`, etc.). If no such CUDA driver -is found, uv will fall back to the CPU-only index. uv will continue to respect existing index -configuration for any packages outside the PyTorch ecosystem. +When enabled, uv will query for the installed CUDA driver and AMD GPU versions then use the +most-compatible PyTorch index for all relevant packages (e.g., `torch`, `torchvision`, etc.). If no +such GPU is found, uv will fall back to the CPU-only index. uv will continue to respect existing +index configuration for any packages outside the PyTorch ecosystem. You can also select a specific backend (e.g., CUDA 12.6) with `--torch-backend=cu126` (or `UV_TORCH_BACKEND=cu126`): @@ -460,5 +460,4 @@ $ # With an environment variable. $ UV_TORCH_BACKEND=cu126 uv pip install torch torchvision ``` -At present, `--torch-backend` is only available in the `uv pip` interface, and only supports -detection of CUDA drivers (as opposed to other accelerators like ROCm or Intel GPUs). +At present, `--torch-backend` is only available in the `uv pip` interface. From 7fce3a88b8e1a947f3c293703e92723e89625662 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 23 Jun 2025 12:04:10 +0200 Subject: [PATCH 063/185] Update aws-actions/configure-aws-credentials digest to 3bb878b (#14203) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | aws-actions/configure-aws-credentials | action | digest | `b475783` -> `3bb878b` | --- > [!WARNING] > Some dependencies could not be looked up. Check the Dependency Dashboard for more information. --- ### Configuration 📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/astral-sh/uv). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 459495600..9e6e2e51c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1443,7 +1443,7 @@ jobs: run: chmod +x ./uv - name: "Configure AWS credentials" - uses: aws-actions/configure-aws-credentials@b47578312673ae6fa5b5096b330d9fbac3d116df + uses: aws-actions/configure-aws-credentials@3bb878b6ab43ba8717918141cd07a0ea68cfe7ea with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} From a52595b61adfc3d4b202f7c37855b31db37a8e2f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 23 Jun 2025 12:04:50 +0200 Subject: [PATCH 064/185] Update google-github-actions/auth digest to 0920706 (#14204) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | google-github-actions/auth | action | digest | `ba79af0` -> `0920706` | --- > [!WARNING] > Some dependencies could not be looked up. Check the Dependency Dashboard for more information. --- ### Configuration 📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/astral-sh/uv). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9e6e2e51c..dd0882f29 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1462,7 +1462,7 @@ jobs: - name: "Authenticate with GCP" id: "auth" - uses: "google-github-actions/auth@ba79af03959ebeac9769e648f473a284504d9193" + uses: "google-github-actions/auth@0920706a19e9d22c3d0da43d1db5939c6ad837a8" with: credentials_json: "${{ secrets.GCP_SERVICE_ACCOUNT_KEY }}" From 46221b40c3cc4ab1e24d5bea8bedb13809536798 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 23 Jun 2025 12:06:20 +0200 Subject: [PATCH 065/185] Update EmbarkStudios/cargo-deny-action action to v2.0.12 (#14206) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [EmbarkStudios/cargo-deny-action](https://redirect.github.com/EmbarkStudios/cargo-deny-action) | action | patch | `v2.0.11` -> `v2.0.12` | --- > [!WARNING] > Some dependencies could not be looked up. Check the Dependency Dashboard for more information. --- ### Release Notes
    EmbarkStudios/cargo-deny-action (EmbarkStudios/cargo-deny-action) ### [`v2.0.12`](https://redirect.github.com/EmbarkStudios/cargo-deny-action/releases/tag/v2.0.12): Release 2.0.12 - cargo-deny 0.18.3 [Compare Source](https://redirect.github.com/EmbarkStudios/cargo-deny-action/compare/v2.0.11...v2.0.12) ##### Changed - [PR#773](https://redirect.github.com/EmbarkStudios/cargo-deny/pull/773) changed cargo-deny's duplicate detection to automatically ignore versions whose only dependent is another version of the same crate.
    --- ### Configuration 📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/astral-sh/uv). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dd0882f29..d730f9190 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -130,7 +130,7 @@ jobs: with: save-if: ${{ github.ref == 'refs/heads/main' }} - name: "Check uv_build dependencies" - uses: EmbarkStudios/cargo-deny-action@34899fc7ba81ca6268d5947a7a16b4649013fea1 # v2.0.11 + uses: EmbarkStudios/cargo-deny-action@30f817c6f72275c6d54dc744fbca09ebc958599f # v2.0.12 with: command: check bans manifest-path: crates/uv-build/Cargo.toml From 3e9dbe8b7d14cf143de9d3b5195c357edc8c7b76 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 23 Jun 2025 12:07:09 +0200 Subject: [PATCH 066/185] Update Rust crate mimalloc to v0.1.47 (#14207) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [mimalloc](https://redirect.github.com/purpleprotocol/mimalloc_rust) | dependencies | patch | `0.1.46` -> `0.1.47` | --- > [!WARNING] > Some dependencies could not be looked up. Check the Dependency Dashboard for more information. --- ### Release Notes
    purpleprotocol/mimalloc_rust (mimalloc) ### [`v0.1.47`](https://redirect.github.com/purpleprotocol/mimalloc_rust/releases/tag/v0.1.47): Version 0.1.47 [Compare Source](https://redirect.github.com/purpleprotocol/mimalloc_rust/compare/v0.1.46...v0.1.47) ##### Changes - Mimalloc `v2.2.4`
    --- ### Configuration 📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/astral-sh/uv). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- crates/uv-performance-memory-allocator/Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/uv-performance-memory-allocator/Cargo.lock b/crates/uv-performance-memory-allocator/Cargo.lock index 831d5a0f9..e1650c824 100644 --- a/crates/uv-performance-memory-allocator/Cargo.lock +++ b/crates/uv-performance-memory-allocator/Cargo.lock @@ -19,9 +19,9 @@ checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "libmimalloc-sys" -version = "0.1.42" +version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec9d6fac27761dabcd4ee73571cdb06b7022dc99089acbe5435691edffaac0f4" +checksum = "bf88cd67e9de251c1781dbe2f641a1a3ad66eaae831b8a2c38fbdc5ddae16d4d" dependencies = [ "cc", "libc", @@ -29,9 +29,9 @@ dependencies = [ [[package]] name = "mimalloc" -version = "0.1.46" +version = "0.1.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "995942f432bbb4822a7e9c3faa87a695185b0d09273ba85f097b54f4e458f2af" +checksum = "b1791cbe101e95af5764f06f20f6760521f7158f69dbf9d6baf941ee1bf6bc40" dependencies = [ "libmimalloc-sys", ] From ac1405e06c628c72cceecdd0dbe889c7e2b6b472 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 23 Jun 2025 12:07:36 +0200 Subject: [PATCH 067/185] Update Rust crate syn to v2.0.104 (#14208) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [syn](https://redirect.github.com/dtolnay/syn) | workspace.dependencies | patch | `2.0.103` -> `2.0.104` | --- > [!WARNING] > Some dependencies could not be looked up. Check the Dependency Dashboard for more information. --- ### Release Notes
    dtolnay/syn (syn) ### [`v2.0.104`](https://redirect.github.com/dtolnay/syn/releases/tag/2.0.104) [Compare Source](https://redirect.github.com/dtolnay/syn/compare/2.0.103...2.0.104) - Disallow attributes on range expression ([#​1872](https://redirect.github.com/dtolnay/syn/issues/1872))
    --- ### Configuration 📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/astral-sh/uv). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6c1978cd0..ce32fb893 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3799,9 +3799,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.103" +version = "2.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4307e30089d6fd6aff212f2da3a1f9e32f3223b1f010fb09b7c95f90f3ca1e8" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" dependencies = [ "proc-macro2", "quote", From a9a9e714819df970ccaa8af7651fa2d1f97a3672 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 23 Jun 2025 10:20:59 +0000 Subject: [PATCH 068/185] Update google-github-actions/setup-gcloud digest to a8b5801 (#14205) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | google-github-actions/setup-gcloud | action | digest | `77e7a55` -> `a8b5801` | --- > [!WARNING] > Some dependencies could not be looked up. Check the Dependency Dashboard for more information. --- ### Configuration 📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/astral-sh/uv). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d730f9190..b33f96fe2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1467,7 +1467,7 @@ jobs: credentials_json: "${{ secrets.GCP_SERVICE_ACCOUNT_KEY }}" - name: "Set up GCP SDK" - uses: "google-github-actions/setup-gcloud@77e7a554d41e2ee56fc945c52dfd3f33d12def9a" + uses: "google-github-actions/setup-gcloud@a8b58010a5b2a061afd605f50e88629c9ec7536b" - name: "Get GCP Artifact Registry token" id: get_token From 6481aa3e64f1fad2f418502cb6b0103353790ee6 Mon Sep 17 00:00:00 2001 From: John Mumm Date: Mon, 23 Jun 2025 09:12:43 -0400 Subject: [PATCH 069/185] Consolidate logic for checking for a virtual environment (#14214) We were checking whether a path was an executable in a virtual environment or the base directory of a virtual environment in multiple places in the codebase. This PR consolidates this logic into one place. Closes #13947. --- crates/uv-fs/src/lib.rs | 24 ++++++++++++++++++++++++ crates/uv-python/src/discovery.rs | 9 ++------- crates/uv-python/src/interpreter.rs | 7 +------ crates/uv-python/src/virtualenv.rs | 4 ++-- crates/uv-virtualenv/src/virtualenv.rs | 2 +- 5 files changed, 30 insertions(+), 16 deletions(-) diff --git a/crates/uv-fs/src/lib.rs b/crates/uv-fs/src/lib.rs index 0b5055b40..ad4a883ad 100644 --- a/crates/uv-fs/src/lib.rs +++ b/crates/uv-fs/src/lib.rs @@ -575,6 +575,30 @@ pub fn is_temporary(path: impl AsRef) -> bool { .is_some_and(|name| name.starts_with(".tmp")) } +/// Checks if the grandparent directory of the given executable is the base +/// of a virtual environment. +/// +/// The procedure described in PEP 405 includes checking both the parent and +/// grandparent directory of an executable, but in practice we've found this to +/// be unnecessary. +pub fn is_virtualenv_executable(executable: impl AsRef) -> bool { + executable + .as_ref() + .parent() + .and_then(Path::parent) + .is_some_and(is_virtualenv_base) +} + +/// Returns `true` if a path is the base path of a virtual environment, +/// indicated by the presence of a `pyvenv.cfg` file. +/// +/// The procedure described in PEP 405 includes scanning `pyvenv.cfg` +/// for a `home` key, but in practice we've found this to be +/// unnecessary. +pub fn is_virtualenv_base(path: impl AsRef) -> bool { + path.as_ref().join("pyvenv.cfg").is_file() +} + /// A file lock that is automatically released when dropped. #[derive(Debug)] pub struct LockedFile(fs_err::File); diff --git a/crates/uv-python/src/discovery.rs b/crates/uv-python/src/discovery.rs index eaf4b4830..d1f3a690a 100644 --- a/crates/uv-python/src/discovery.rs +++ b/crates/uv-python/src/discovery.rs @@ -888,13 +888,8 @@ impl Error { | InterpreterError::BrokenSymlink(BrokenSymlink { path, .. }) => { // If the interpreter is from an active, valid virtual environment, we should // fail because it's broken - if let Some(Ok(true)) = matches!(source, PythonSource::ActiveEnvironment) - .then(|| { - path.parent() - .and_then(Path::parent) - .map(|path| path.join("pyvenv.cfg").try_exists()) - }) - .flatten() + if matches!(source, PythonSource::ActiveEnvironment) + && uv_fs::is_virtualenv_executable(path) { true } else { diff --git a/crates/uv-python/src/interpreter.rs b/crates/uv-python/src/interpreter.rs index 19e790b7e..b62633283 100644 --- a/crates/uv-python/src/interpreter.rs +++ b/crates/uv-python/src/interpreter.rs @@ -993,14 +993,9 @@ impl InterpreterInfo { .symlink_metadata() .is_ok_and(|metadata| metadata.is_symlink()) { - let venv = executable - .parent() - .and_then(Path::parent) - .map(|path| path.join("pyvenv.cfg").is_file()) - .unwrap_or(false); Error::BrokenSymlink(BrokenSymlink { path: executable.to_path_buf(), - venv, + venv: uv_fs::is_virtualenv_executable(executable), }) } else { Error::NotFound(executable.to_path_buf()) diff --git a/crates/uv-python/src/virtualenv.rs b/crates/uv-python/src/virtualenv.rs index ea578fff3..8b51a5e1b 100644 --- a/crates/uv-python/src/virtualenv.rs +++ b/crates/uv-python/src/virtualenv.rs @@ -130,14 +130,14 @@ pub(crate) fn virtualenv_from_working_dir() -> Result, Error> { for dir in current_dir.ancestors() { // If we're _within_ a virtualenv, return it. - if dir.join("pyvenv.cfg").is_file() { + if uv_fs::is_virtualenv_base(dir) { return Ok(Some(dir.to_path_buf())); } // Otherwise, search for a `.venv` directory. let dot_venv = dir.join(".venv"); if dot_venv.is_dir() { - if !dot_venv.join("pyvenv.cfg").is_file() { + if !uv_fs::is_virtualenv_base(&dot_venv) { return Err(Error::MissingPyVenvCfg(dot_venv)); } return Ok(Some(dot_venv)); diff --git a/crates/uv-virtualenv/src/virtualenv.rs b/crates/uv-virtualenv/src/virtualenv.rs index bb6db01a3..f466233c0 100644 --- a/crates/uv-virtualenv/src/virtualenv.rs +++ b/crates/uv-virtualenv/src/virtualenv.rs @@ -85,7 +85,7 @@ pub(crate) fn create( } else if metadata.is_dir() { if allow_existing { debug!("Allowing existing directory"); - } else if location.join("pyvenv.cfg").is_file() { + } else if uv_fs::is_virtualenv_base(location) { debug!("Removing existing directory"); // On Windows, if the current executable is in the directory, guard against From b06dec8398af0ef5e5189ac2c4085cfb9f9531f7 Mon Sep 17 00:00:00 2001 From: John Mumm Date: Mon, 23 Jun 2025 09:51:44 -0400 Subject: [PATCH 070/185] Improve Python uninstall perf by removing unnecessary call to `installations.find_all()` (#14180) #13954 introduced an unnecessary slow-down to Python uninstall by calling `installations.find_all()` to discover remaining installations after an uninstall. Instead, we can filter all initial installations against those in `uninstalled`. As part of this change, I've updated `uninstalled` from a `Vec` to an `IndexSet` in order to do efficient lookups in the filter. This required a change I call out below to how we were retrieving them for messaging. --- crates/uv/src/commands/python/uninstall.rs | 30 +++++++++++----------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/crates/uv/src/commands/python/uninstall.rs b/crates/uv/src/commands/python/uninstall.rs index e79ea9b19..642942d07 100644 --- a/crates/uv/src/commands/python/uninstall.rs +++ b/crates/uv/src/commands/python/uninstall.rs @@ -200,13 +200,13 @@ async fn do_uninstall( }); } - let mut uninstalled = vec![]; + let mut uninstalled = IndexSet::::default(); let mut errors = vec![]; while let Some((key, result)) = tasks.next().await { if let Err(err) = result { errors.push((key.clone(), anyhow::Error::new(err))); } else { - uninstalled.push(key.clone()); + uninstalled.insert(key.clone()); } } @@ -223,14 +223,15 @@ async fn do_uninstall( // Read all existing managed installations and find the highest installed patch // for each installed minor version. Ensure the minor version link directory // is still valid. - let uninstalled_minor_versions = &uninstalled.iter().fold( - IndexSet::<&PythonInstallationMinorVersionKey>::default(), - |mut minor_versions, key| { - minor_versions.insert(PythonInstallationMinorVersionKey::ref_cast(key)); - minor_versions - }, - ); - let remaining_installations: Vec<_> = installations.find_all()?.collect(); + let uninstalled_minor_versions: IndexSet<_> = uninstalled + .iter() + .map(PythonInstallationMinorVersionKey::ref_cast) + .collect(); + let remaining_installations: Vec<_> = installed_installations + .into_iter() + .filter(|installation| !uninstalled.contains(installation.key())) + .collect(); + let remaining_minor_versions = PythonInstallationMinorVersionKey::highest_installations_by_minor_version_key( remaining_installations.iter(), @@ -278,28 +279,27 @@ async fn do_uninstall( } // Report on any uninstalled installations. - if !uninstalled.is_empty() { - if let [uninstalled] = uninstalled.as_slice() { + if let Some(first_uninstalled) = uninstalled.first() { + if uninstalled.len() == 1 { // Ex) "Uninstalled Python 3.9.7 in 1.68s" writeln!( printer.stderr(), "{}", format!( "Uninstalled {} {}", - format!("Python {}", uninstalled.version()).bold(), + format!("Python {}", first_uninstalled.version()).bold(), format!("in {}", elapsed(start.elapsed())).dimmed() ) .dimmed() )?; } else { // Ex) "Uninstalled 2 versions in 1.68s" - let s = if uninstalled.len() == 1 { "" } else { "s" }; writeln!( printer.stderr(), "{}", format!( "Uninstalled {} {}", - format!("{} version{s}", uninstalled.len()).bold(), + format!("{} versions", uninstalled.len()).bold(), format!("in {}", elapsed(start.elapsed())).dimmed() ) .dimmed() From 2d2dd0c1a38b81f14484dd5bc37d2eb64d928948 Mon Sep 17 00:00:00 2001 From: John Mumm Date: Mon, 23 Jun 2025 12:19:36 -0400 Subject: [PATCH 071/185] Update upgrade tests to use 3.10.17 instead of 3.10.8 (#14219) @oconnor663 discovered that executing `3.10.8` on Arch Linux ran into an error loading `libcrypt.so.1`. This caused uv to install the latest patch version on `uv venv` operations during upgrade tests, which undermined their purpose (since they are checking that if you first install `3.10.8` and then upgrade, virtual environments are transparently upgraded). This PR updates the test to use `3.10.17` instead to avoid this issue. --- crates/uv/tests/it/python_install.rs | 4 +- crates/uv/tests/it/python_upgrade.rs | 82 ++++++++++++++-------------- 2 files changed, 43 insertions(+), 43 deletions(-) diff --git a/crates/uv/tests/it/python_install.rs b/crates/uv/tests/it/python_install.rs index d5dec1977..2b6f03d4b 100644 --- a/crates/uv/tests/it/python_install.rs +++ b/crates/uv/tests/it/python_install.rs @@ -1742,14 +1742,14 @@ fn install_multiple_patches() { fs_err::remove_dir_all(&context.venv).unwrap(); // Install 3.10 patches in descending order list - uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.10.17").arg("3.10.8"), @r" + uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.10.17").arg("3.10.16"), @r" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Installed 2 versions in [TIME] - + cpython-3.10.8-[PLATFORM] + + cpython-3.10.16-[PLATFORM] + cpython-3.10.17-[PLATFORM] (python3.10) " ); diff --git a/crates/uv/tests/it/python_upgrade.rs b/crates/uv/tests/it/python_upgrade.rs index cbea1d404..bf6d45e08 100644 --- a/crates/uv/tests/it/python_upgrade.rs +++ b/crates/uv/tests/it/python_upgrade.rs @@ -13,18 +13,18 @@ fn python_upgrade() { .with_managed_python_dirs(); // Install an earlier patch version - uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.10.8"), @r" + uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.10.17"), @r" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- - Installed Python 3.10.8 in [TIME] - + cpython-3.10.8-[PLATFORM] (python3.10) + Installed Python 3.10.17 in [TIME] + + cpython-3.10.17-[PLATFORM] (python3.10) "); // Don't accept patch version as argument to upgrade command - uv_snapshot!(context.filters(), context.python_upgrade().arg("--preview").arg("3.10.8"), @r" + uv_snapshot!(context.filters(), context.python_upgrade().arg("--preview").arg("3.10.17"), @r" success: false exit_code: 1 ----- stdout ----- @@ -130,14 +130,14 @@ fn python_upgrade_transparent_from_venv() { .with_managed_python_dirs(); // Install an earlier patch version - uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.10.8"), @r" + uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.10.17"), @r" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- - Installed Python 3.10.8 in [TIME] - + cpython-3.10.8-[PLATFORM] (python3.10) + Installed Python 3.10.17 in [TIME] + + cpython-3.10.17-[PLATFORM] (python3.10) "); // Create a virtual environment @@ -147,7 +147,7 @@ fn python_upgrade_transparent_from_venv() { ----- stdout ----- ----- stderr ----- - Using CPython 3.10.8 + Using CPython 3.10.17 Creating virtual environment at: .venv Activate with: source .venv/[BIN]/activate "); @@ -156,7 +156,7 @@ fn python_upgrade_transparent_from_venv() { success: true exit_code: 0 ----- stdout ----- - Python 3.10.8 + Python 3.10.17 ----- stderr ----- " @@ -171,7 +171,7 @@ fn python_upgrade_transparent_from_venv() { ----- stdout ----- ----- stderr ----- - Using CPython 3.10.8 + Using CPython 3.10.17 Creating virtual environment at: .venv2 Activate with: source .venv2/[BIN]/activate "); @@ -181,7 +181,7 @@ fn python_upgrade_transparent_from_venv() { success: true exit_code: 0 ----- stdout ----- - Python 3.10.8 + Python 3.10.17 ----- stderr ----- " @@ -232,14 +232,14 @@ fn python_upgrade_transparent_from_venv_preview() { .with_managed_python_dirs(); // Install an earlier patch version using `--preview` - uv_snapshot!(context.filters(), context.python_install().arg("3.10.8").arg("--preview"), @r" + uv_snapshot!(context.filters(), context.python_install().arg("3.10.17").arg("--preview"), @r" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- - Installed Python 3.10.8 in [TIME] - + cpython-3.10.8-[PLATFORM] (python3.10) + Installed Python 3.10.17 in [TIME] + + cpython-3.10.17-[PLATFORM] (python3.10) "); // Create a virtual environment @@ -249,7 +249,7 @@ fn python_upgrade_transparent_from_venv_preview() { ----- stdout ----- ----- stderr ----- - Using CPython 3.10.8 + Using CPython 3.10.17 Creating virtual environment at: .venv Activate with: source .venv/[BIN]/activate "); @@ -258,7 +258,7 @@ fn python_upgrade_transparent_from_venv_preview() { success: true exit_code: 0 ----- stdout ----- - Python 3.10.8 + Python 3.10.17 ----- stderr ----- " @@ -295,14 +295,14 @@ fn python_upgrade_ignored_with_python_pin() { .with_managed_python_dirs(); // Install an earlier patch version - uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.10.8"), @r" + uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.10.17"), @r" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- - Installed Python 3.10.8 in [TIME] - + cpython-3.10.8-[PLATFORM] (python3.10) + Installed Python 3.10.17 in [TIME] + + cpython-3.10.17-[PLATFORM] (python3.10) "); // Create a virtual environment @@ -312,17 +312,17 @@ fn python_upgrade_ignored_with_python_pin() { ----- stdout ----- ----- stderr ----- - Using CPython 3.10.8 + Using CPython 3.10.17 Creating virtual environment at: .venv Activate with: source .venv/[BIN]/activate "); // Pin to older patch version - uv_snapshot!(context.filters(), context.python_pin().arg("3.10.8"), @r" + uv_snapshot!(context.filters(), context.python_pin().arg("3.10.17"), @r" success: true exit_code: 0 ----- stdout ----- - Pinned `.python-version` to `3.10.8` + Pinned `.python-version` to `3.10.17` ----- stderr ----- "); @@ -343,7 +343,7 @@ fn python_upgrade_ignored_with_python_pin() { success: true exit_code: 0 ----- stdout ----- - Python 3.10.8 + Python 3.10.17 ----- stderr ----- " @@ -360,24 +360,24 @@ fn python_no_transparent_upgrade_with_venv_patch_specification() { .with_managed_python_dirs(); // Install an earlier patch version - uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.10.8"), @r" + uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.10.17"), @r" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- - Installed Python 3.10.8 in [TIME] - + cpython-3.10.8-[PLATFORM] (python3.10) + Installed Python 3.10.17 in [TIME] + + cpython-3.10.17-[PLATFORM] (python3.10) "); // Create a virtual environment with a patch version - uv_snapshot!(context.filters(), context.venv().arg("-p").arg("3.10.8"), @r" + uv_snapshot!(context.filters(), context.venv().arg("-p").arg("3.10.17"), @r" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- - Using CPython 3.10.8 + Using CPython 3.10.17 Creating virtual environment at: .venv Activate with: source .venv/[BIN]/activate "); @@ -386,7 +386,7 @@ fn python_no_transparent_upgrade_with_venv_patch_specification() { success: true exit_code: 0 ----- stdout ----- - Python 3.10.8 + Python 3.10.17 ----- stderr ----- " @@ -408,7 +408,7 @@ fn python_no_transparent_upgrade_with_venv_patch_specification() { success: true exit_code: 0 ----- stdout ----- - Python 3.10.8 + Python 3.10.17 ----- stderr ----- " @@ -426,14 +426,14 @@ fn python_transparent_upgrade_venv_venv() { .with_managed_python_dirs(); // Install an earlier patch version - uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.10.8"), @r" + uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.10.17"), @r" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- - Installed Python 3.10.8 in [TIME] - + cpython-3.10.8-[PLATFORM] (python3.10) + Installed Python 3.10.17 in [TIME] + + cpython-3.10.17-[PLATFORM] (python3.10) "); // Create an initial virtual environment @@ -443,7 +443,7 @@ fn python_transparent_upgrade_venv_venv() { ----- stdout ----- ----- stderr ----- - Using CPython 3.10.8 + Using CPython 3.10.17 Creating virtual environment at: .venv Activate with: source .venv/[BIN]/activate "); @@ -465,7 +465,7 @@ fn python_transparent_upgrade_venv_venv() { ----- stdout ----- ----- stderr ----- - Using CPython 3.10.8 interpreter at: .venv/[BIN]/python + Using CPython 3.10.17 interpreter at: .venv/[BIN]/python Creating virtual environment at: .venv2 Activate with: source .venv2/[BIN]/activate "); @@ -477,7 +477,7 @@ fn python_transparent_upgrade_venv_venv() { success: true exit_code: 0 ----- stdout ----- - Python 3.10.8 + Python 3.10.17 ----- stderr ----- " @@ -588,14 +588,14 @@ fn python_upgrade_transparent_from_venv_module_in_venv() { let bin_dir = context.temp_dir.child("bin"); // Install earlier patch version - uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.10.8"), @r" + uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.10.17"), @r" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- - Installed Python 3.10.8 in [TIME] - + cpython-3.10.8-[PLATFORM] (python3.10) + Installed Python 3.10.17 in [TIME] + + cpython-3.10.17-[PLATFORM] (python3.10) "); // Create first virtual environment @@ -605,7 +605,7 @@ fn python_upgrade_transparent_from_venv_module_in_venv() { ----- stdout ----- ----- stderr ----- - Using CPython 3.10.8 + Using CPython 3.10.17 Creating virtual environment at: .venv Activate with: source .venv/[BIN]/activate "); @@ -630,7 +630,7 @@ fn python_upgrade_transparent_from_venv_module_in_venv() { success: true exit_code: 0 ----- stdout ----- - Python 3.10.8 + Python 3.10.17 ----- stderr ----- " From 92de53f4eb9c3869bf36c91b2dcd9f3556eee2bc Mon Sep 17 00:00:00 2001 From: Aria Desires Date: Mon, 23 Jun 2025 12:48:51 -0400 Subject: [PATCH 072/185] Bump version to 0.7.14 (#14218) --- CHANGELOG.md | 38 ++++++++++++++++++++++++++- Cargo.lock | 6 ++--- crates/uv-build/Cargo.toml | 2 +- crates/uv-build/pyproject.toml | 2 +- crates/uv-version/Cargo.toml | 2 +- crates/uv/Cargo.toml | 2 +- docs/concepts/build-backend.md | 2 +- docs/getting-started/installation.md | 4 +-- docs/guides/integration/aws-lambda.md | 4 +-- docs/guides/integration/docker.md | 10 +++---- docs/guides/integration/github.md | 2 +- docs/guides/integration/pre-commit.md | 10 +++---- pyproject.toml | 2 +- 13 files changed, 61 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f62cedf62..159c39331 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,8 +3,44 @@ -## 0.7.13 +## 0.7.14 +### Enhancements + +- Add XPU to `--torch-backend` ([#14172](https://github.com/astral-sh/uv/pull/14172)) +- Add ROCm backends to `--torch-backend` ([#14120](https://github.com/astral-sh/uv/pull/14120)) +- Remove preview label from `--torch-backend` ([#14119](https://github.com/astral-sh/uv/pull/14119)) +- Add `[tool.uv.dependency-groups].mygroup.requires-python` ([#13735](https://github.com/astral-sh/uv/pull/13735)) +- Add auto-detection for AMD GPUs ([#14176](https://github.com/astral-sh/uv/pull/14176)) +- Show retries for HTTP status code errors ([#13897](https://github.com/astral-sh/uv/pull/13897)) +- Support transparent Python patch version upgrades ([#13954](https://github.com/astral-sh/uv/pull/13954)) +- Warn on empty index directory ([#13940](https://github.com/astral-sh/uv/pull/13940)) +- Publish to DockerHub ([#14088](https://github.com/astral-sh/uv/pull/14088)) + +### Performance + +- Make cold resolves about 10% faster ([#14035](https://github.com/astral-sh/uv/pull/14035)) + +### Bug fixes + +- Don't use walrus operator in interpreter query script ([#14108](https://github.com/astral-sh/uv/pull/14108)) +- Fix handling of changes to `requires-python` ([#14076](https://github.com/astral-sh/uv/pull/14076)) +- Fix implied `platform_machine` marker for `win_amd64` platform tag ([#14041](https://github.com/astral-sh/uv/pull/14041)) +- Only update existing symlink directories on preview uninstall ([#14179](https://github.com/astral-sh/uv/pull/14179)) +- Serialize Python requests for tools as canonicalized strings ([#14109](https://github.com/astral-sh/uv/pull/14109)) +- Support netrc and same-origin credential propagation on index redirects ([#14126](https://github.com/astral-sh/uv/pull/14126)) +- Support reading `dependency-groups` from pyproject.tomls with no `[project]` ([#13742](https://github.com/astral-sh/uv/pull/13742)) +- Handle an existing shebang in `uv init --script` ([#14141](https://github.com/astral-sh/uv/pull/14141)) +- Prevent concurrent updates of the environment in `uv run` ([#14153](https://github.com/astral-sh/uv/pull/14153)) +- Filter managed Python distributions by platform before querying when included in request ([#13936](https://github.com/astral-sh/uv/pull/13936)) + +### Documentation + +- Replace cuda124 with cuda128 ([#14168](https://github.com/astral-sh/uv/pull/14168)) +- Document the way member sources shadow workspace sources ([#14136](https://github.com/astral-sh/uv/pull/14136)) +- Sync documented PyTorch integration index for CUDA and ROCm versions from PyTorch website ([#14100](https://github.com/astral-sh/uv/pull/14100)) + +## 0.7.13 ### Python diff --git a/Cargo.lock b/Cargo.lock index ce32fb893..95e9308b4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4569,7 +4569,7 @@ dependencies = [ [[package]] name = "uv" -version = "0.7.13" +version = "0.7.14" dependencies = [ "anstream", "anyhow", @@ -4735,7 +4735,7 @@ dependencies = [ [[package]] name = "uv-build" -version = "0.7.13" +version = "0.7.14" dependencies = [ "anyhow", "uv-build-backend", @@ -5923,7 +5923,7 @@ dependencies = [ [[package]] name = "uv-version" -version = "0.7.13" +version = "0.7.14" [[package]] name = "uv-virtualenv" diff --git a/crates/uv-build/Cargo.toml b/crates/uv-build/Cargo.toml index 01f6f7cbe..26c046b2b 100644 --- a/crates/uv-build/Cargo.toml +++ b/crates/uv-build/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-build" -version = "0.7.13" +version = "0.7.14" edition.workspace = true rust-version.workspace = true homepage.workspace = true diff --git a/crates/uv-build/pyproject.toml b/crates/uv-build/pyproject.toml index 7e556e6bf..4dab6a520 100644 --- a/crates/uv-build/pyproject.toml +++ b/crates/uv-build/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uv-build" -version = "0.7.13" +version = "0.7.14" description = "The uv build backend" authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }] requires-python = ">=3.8" diff --git a/crates/uv-version/Cargo.toml b/crates/uv-version/Cargo.toml index a029b8196..b92014be4 100644 --- a/crates/uv-version/Cargo.toml +++ b/crates/uv-version/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-version" -version = "0.7.13" +version = "0.7.14" edition = { workspace = true } rust-version = { workspace = true } homepage = { workspace = true } diff --git a/crates/uv/Cargo.toml b/crates/uv/Cargo.toml index de4fa3c38..e63cbfe40 100644 --- a/crates/uv/Cargo.toml +++ b/crates/uv/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv" -version = "0.7.13" +version = "0.7.14" edition = { workspace = true } rust-version = { workspace = true } homepage = { workspace = true } diff --git a/docs/concepts/build-backend.md b/docs/concepts/build-backend.md index 8d0dfa533..2162aaaa5 100644 --- a/docs/concepts/build-backend.md +++ b/docs/concepts/build-backend.md @@ -19,7 +19,7 @@ existing project, add it to the `[build-system]` section in your `pyproject.toml ```toml [build-system] -requires = ["uv_build>=0.7.13,<0.8.0"] +requires = ["uv_build>=0.7.14,<0.8.0"] build-backend = "uv_build" ``` diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 6b6b3e872..a87ce695a 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -25,7 +25,7 @@ uv provides a standalone installer to download and install uv: Request a specific version by including it in the URL: ```console - $ curl -LsSf https://astral.sh/uv/0.7.13/install.sh | sh + $ curl -LsSf https://astral.sh/uv/0.7.14/install.sh | sh ``` === "Windows" @@ -41,7 +41,7 @@ uv provides a standalone installer to download and install uv: Request a specific version by including it in the URL: ```pwsh-session - PS> powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/0.7.13/install.ps1 | iex" + PS> powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/0.7.14/install.ps1 | iex" ``` !!! tip diff --git a/docs/guides/integration/aws-lambda.md b/docs/guides/integration/aws-lambda.md index 700011297..467088736 100644 --- a/docs/guides/integration/aws-lambda.md +++ b/docs/guides/integration/aws-lambda.md @@ -92,7 +92,7 @@ the second stage, we'll copy this directory over to the final image, omitting th other unnecessary files. ```dockerfile title="Dockerfile" -FROM ghcr.io/astral-sh/uv:0.7.13 AS uv +FROM ghcr.io/astral-sh/uv:0.7.14 AS uv # First, bundle the dependencies into the task root. FROM public.ecr.aws/lambda/python:3.13 AS builder @@ -334,7 +334,7 @@ And confirm that opening http://127.0.0.1:8000/ in a web browser displays, "Hell Finally, we'll update the Dockerfile to include the local library in the deployment package: ```dockerfile title="Dockerfile" -FROM ghcr.io/astral-sh/uv:0.7.13 AS uv +FROM ghcr.io/astral-sh/uv:0.7.14 AS uv # First, bundle the dependencies into the task root. FROM public.ecr.aws/lambda/python:3.13 AS builder diff --git a/docs/guides/integration/docker.md b/docs/guides/integration/docker.md index 55f86fad6..01e66f775 100644 --- a/docs/guides/integration/docker.md +++ b/docs/guides/integration/docker.md @@ -31,7 +31,7 @@ $ docker run --rm -it ghcr.io/astral-sh/uv:debian uv --help The following distroless images are available: - `ghcr.io/astral-sh/uv:latest` -- `ghcr.io/astral-sh/uv:{major}.{minor}.{patch}`, e.g., `ghcr.io/astral-sh/uv:0.7.13` +- `ghcr.io/astral-sh/uv:{major}.{minor}.{patch}`, e.g., `ghcr.io/astral-sh/uv:0.7.14` - `ghcr.io/astral-sh/uv:{major}.{minor}`, e.g., `ghcr.io/astral-sh/uv:0.7` (the latest patch version) @@ -75,7 +75,7 @@ And the following derived images are available: As with the distroless image, each derived image is published with uv version tags as `ghcr.io/astral-sh/uv:{major}.{minor}.{patch}-{base}` and -`ghcr.io/astral-sh/uv:{major}.{minor}-{base}`, e.g., `ghcr.io/astral-sh/uv:0.7.13-alpine`. +`ghcr.io/astral-sh/uv:{major}.{minor}-{base}`, e.g., `ghcr.io/astral-sh/uv:0.7.14-alpine`. For more details, see the [GitHub Container](https://github.com/astral-sh/uv/pkgs/container/uv) page. @@ -113,7 +113,7 @@ Note this requires `curl` to be available. In either case, it is best practice to pin to a specific uv version, e.g., with: ```dockerfile -COPY --from=ghcr.io/astral-sh/uv:0.7.13 /uv /uvx /bin/ +COPY --from=ghcr.io/astral-sh/uv:0.7.14 /uv /uvx /bin/ ``` !!! tip @@ -131,7 +131,7 @@ COPY --from=ghcr.io/astral-sh/uv:0.7.13 /uv /uvx /bin/ Or, with the installer: ```dockerfile -ADD https://astral.sh/uv/0.7.13/install.sh /uv-installer.sh +ADD https://astral.sh/uv/0.7.14/install.sh /uv-installer.sh ``` ### Installing a project @@ -557,5 +557,5 @@ Verified OK !!! tip These examples use `latest`, but best practice is to verify the attestation for a specific - version tag, e.g., `ghcr.io/astral-sh/uv:0.7.13`, or (even better) the specific image digest, + version tag, e.g., `ghcr.io/astral-sh/uv:0.7.14`, or (even better) the specific image digest, such as `ghcr.io/astral-sh/uv:0.5.27@sha256:5adf09a5a526f380237408032a9308000d14d5947eafa687ad6c6a2476787b4f`. diff --git a/docs/guides/integration/github.md b/docs/guides/integration/github.md index 49eb57a80..0bd1cc1b1 100644 --- a/docs/guides/integration/github.md +++ b/docs/guides/integration/github.md @@ -47,7 +47,7 @@ jobs: uses: astral-sh/setup-uv@v5 with: # Install a specific version of uv. - version: "0.7.13" + version: "0.7.14" ``` ## Setting up Python diff --git a/docs/guides/integration/pre-commit.md b/docs/guides/integration/pre-commit.md index 87c384da0..326832e7e 100644 --- a/docs/guides/integration/pre-commit.md +++ b/docs/guides/integration/pre-commit.md @@ -19,7 +19,7 @@ To make sure your `uv.lock` file is up to date even if your `pyproject.toml` fil repos: - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.7.13 + rev: 0.7.14 hooks: - id: uv-lock ``` @@ -30,7 +30,7 @@ To keep a `requirements.txt` file in sync with your `uv.lock` file: repos: - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.7.13 + rev: 0.7.14 hooks: - id: uv-export ``` @@ -41,7 +41,7 @@ To compile requirements files: repos: - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.7.13 + rev: 0.7.14 hooks: # Compile requirements - id: pip-compile @@ -54,7 +54,7 @@ To compile alternative requirements files, modify `args` and `files`: repos: - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.7.13 + rev: 0.7.14 hooks: # Compile requirements - id: pip-compile @@ -68,7 +68,7 @@ To run the hook over multiple files at the same time, add additional entries: repos: - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.7.13 + rev: 0.7.14 hooks: # Compile requirements - id: pip-compile diff --git a/pyproject.toml b/pyproject.toml index d7bc4d805..86dd0a1e2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "uv" -version = "0.7.13" +version = "0.7.14" description = "An extremely fast Python package and project manager, written in Rust." authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }] requires-python = ">=3.8" From e7f596711114528d98af1d20c91ce05f8462c703 Mon Sep 17 00:00:00 2001 From: Aria Desires Date: Mon, 23 Jun 2025 13:30:49 -0400 Subject: [PATCH 073/185] Don't block docker logins on it being a pull-request (#14222) --- .github/workflows/build-docker.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml index 25e042275..9a2e5ba1d 100644 --- a/.github/workflows/build-docker.yml +++ b/.github/workflows/build-docker.yml @@ -90,7 +90,6 @@ jobs: # Login to DockerHub (when not pushing, it's to avoid rate-limiting) - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 - if: ${{ github.event.pull_request.head.repo.full_name == 'astral-sh/uv' }} with: username: ${{ needs.docker-plan.outputs.push == 'true' && 'astral' || 'astralshbot' }} password: ${{ needs.docker-plan.outputs.push == 'true' && secrets.DOCKERHUB_TOKEN_RW || secrets.DOCKERHUB_TOKEN_RO }} @@ -196,7 +195,6 @@ jobs: steps: # Login to DockerHub (when not pushing, it's to avoid rate-limiting) - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 - if: ${{ github.event.pull_request.head.repo.full_name == 'astral-sh/uv' }} with: username: ${{ needs.docker-plan.outputs.push == 'true' && 'astral' || 'astralshbot' }} password: ${{ needs.docker-plan.outputs.push == 'true' && secrets.DOCKERHUB_TOKEN_RW || secrets.DOCKERHUB_TOKEN_RO }} From d9351d52fc7acd355a9577355fea19df3df79626 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 23 Jun 2025 14:26:14 -0400 Subject: [PATCH 074/185] Remove wheel filename-from URL conversion (#14223) ## Summary This appears to be unused. --- Cargo.lock | 1 - crates/uv-distribution-filename/Cargo.toml | 1 - crates/uv-distribution-filename/src/wheel.rs | 24 -------------------- 3 files changed, 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 95e9308b4..6a717094c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5135,7 +5135,6 @@ dependencies = [ "serde", "smallvec", "thiserror 2.0.12", - "url", "uv-cache-key", "uv-normalize", "uv-pep440", diff --git a/crates/uv-distribution-filename/Cargo.toml b/crates/uv-distribution-filename/Cargo.toml index f30e79b3b..0dfdd623e 100644 --- a/crates/uv-distribution-filename/Cargo.toml +++ b/crates/uv-distribution-filename/Cargo.toml @@ -27,7 +27,6 @@ rkyv = { workspace = true, features = ["smallvec-1"] } serde = { workspace = true } smallvec = { workspace = true } thiserror = { workspace = true } -url = { workspace = true } [dev-dependencies] insta = { version = "1.40.0" } diff --git a/crates/uv-distribution-filename/src/wheel.rs b/crates/uv-distribution-filename/src/wheel.rs index d7dc7dfca..2ac0ef7d9 100644 --- a/crates/uv-distribution-filename/src/wheel.rs +++ b/crates/uv-distribution-filename/src/wheel.rs @@ -5,7 +5,6 @@ use std::str::FromStr; use memchr::memchr; use serde::{Deserialize, Deserializer, Serialize, Serializer, de}; use thiserror::Error; -use url::Url; use uv_cache_key::cache_digest; use uv_normalize::{InvalidNameError, PackageName}; @@ -300,29 +299,6 @@ impl WheelFilename { } } -impl TryFrom<&Url> for WheelFilename { - type Error = WheelFilenameError; - - fn try_from(url: &Url) -> Result { - let filename = url - .path_segments() - .ok_or_else(|| { - WheelFilenameError::InvalidWheelFileName( - url.to_string(), - "URL must have a path".to_string(), - ) - })? - .next_back() - .ok_or_else(|| { - WheelFilenameError::InvalidWheelFileName( - url.to_string(), - "URL must contain a filename".to_string(), - ) - })?; - Self::from_str(filename) - } -} - impl<'de> Deserialize<'de> for WheelFilename { fn deserialize(deserializer: D) -> Result where From aa2448ef836af43f4acd7ac7271c680174a76c76 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 23 Jun 2025 14:52:07 -0400 Subject: [PATCH 075/185] Strip query parameters when parsing source URL (#14224) ## Summary Closes https://github.com/astral-sh/uv/issues/14217. --- crates/uv-resolver/src/lock/mod.rs | 40 +++++++++++++++++++++++++----- crates/uv/tests/it/sync.rs | 29 ++++++++++++++++++++++ 2 files changed, 63 insertions(+), 6 deletions(-) diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index 5af4377b9..0b72014a8 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -2371,7 +2371,13 @@ impl Package { let sdist = match &self.id.source { Source::Path(path) => { // A direct path source can also be a wheel, so validate the extension. - let DistExtension::Source(ext) = DistExtension::from_path(path)? else { + let DistExtension::Source(ext) = DistExtension::from_path(path).map_err(|err| { + LockErrorKind::MissingExtension { + id: self.id.clone(), + err, + } + })? + else { return Ok(None); }; let install_path = absolute_path(workspace_root, path)?; @@ -2444,7 +2450,14 @@ impl Package { } Source::Direct(url, direct) => { // A direct URL source can also be a wheel, so validate the extension. - let DistExtension::Source(ext) = DistExtension::from_path(url.as_ref())? else { + let DistExtension::Source(ext) = + DistExtension::from_path(url.base_str()).map_err(|err| { + LockErrorKind::MissingExtension { + id: self.id.clone(), + err, + } + })? + else { return Ok(None); }; let location = url.to_url().map_err(LockErrorKind::InvalidUrl)?; @@ -2483,7 +2496,12 @@ impl Package { .ok_or_else(|| LockErrorKind::MissingFilename { id: self.id.clone(), })?; - let ext = SourceDistExtension::from_path(filename.as_ref())?; + let ext = SourceDistExtension::from_path(filename.as_ref()).map_err(|err| { + LockErrorKind::MissingExtension { + id: self.id.clone(), + err, + } + })?; let file = Box::new(uv_distribution_types::File { dist_info_metadata: false, filename: SmallString::from(filename), @@ -2535,7 +2553,12 @@ impl Package { .ok_or_else(|| LockErrorKind::MissingFilename { id: self.id.clone(), })?; - let ext = SourceDistExtension::from_path(filename.as_ref())?; + let ext = SourceDistExtension::from_path(filename.as_ref()).map_err(|err| { + LockErrorKind::MissingExtension { + id: self.id.clone(), + err, + } + })?; let file = Box::new(uv_distribution_types::File { dist_info_metadata: false, filename: SmallString::from(filename), @@ -5222,8 +5245,13 @@ enum LockErrorKind { ), /// An error that occurs when the extension can't be determined /// for a given wheel or source distribution. - #[error("Failed to parse file extension; expected one of: {0}")] - MissingExtension(#[from] ExtensionError), + #[error("Failed to parse file extension for `{id}`; expected one of: {err}", id = id.cyan())] + MissingExtension { + /// The filename that was expected to have an extension. + id: PackageId, + /// The list of valid extensions that were expected. + err: ExtensionError, + }, /// Failed to parse a Git source URL. #[error("Failed to parse Git URL")] InvalidGitSourceUrl( diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index e6fe831d3..9da2fb62a 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -9943,3 +9943,32 @@ fn sync_required_environment_hint() -> Result<()> { Ok(()) } + +#[test] +fn sync_url_with_query_parameters() -> Result<()> { + let context = TestContext::new("3.13").with_exclude_newer("2025-03-24T19:00:00Z"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(r#" + [project] + name = "example" + version = "0.1.0" + requires-python = ">=3.13.2" + dependencies = ["source-distribution @ https://files.pythonhosted.org/packages/1f/e5/5b016c945d745f8b108e759d428341488a6aee8f51f07c6c4e33498bb91f/source_distribution-0.0.3.tar.gz?foo=bar"] + "# + )?; + + uv_snapshot!(context.filters(), context.sync(), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + source-distribution==0.0.3 (from https://files.pythonhosted.org/packages/1f/e5/5b016c945d745f8b108e759d428341488a6aee8f51f07c6c4e33498bb91f/source_distribution-0.0.3.tar.gz?foo=bar) + "); + + Ok(()) +} From 19c58c7fbbe7f7fbf5a4c1db806ed55fcc3a9072 Mon Sep 17 00:00:00 2001 From: Ben Beasley Date: Tue, 24 Jun 2025 09:04:55 -0400 Subject: [PATCH 076/185] Update wiremock to 0.6.4 (#14238) ## Summary In e10881d49c8649a66f301af9052fd2bb17eb68d4, `uv` started using a fork of the `wiremock` crate, https://github.com/astral-sh/wiremock-rs, linking companion PR https://github.com/LukeMathWalker/wiremock-rs/pull/159. That PR was merged in `wiremock` 0.6.4, so this PR switches back to the crates.io version of `wiremock`, with a minimum version of 0.6.4. ## Test Plan ``` $ cargo run python install $ cargo test ```` --- Cargo.lock | 5 +++-- Cargo.toml | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6a717094c..0680e0c28 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6700,8 +6700,9 @@ checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" [[package]] name = "wiremock" -version = "0.6.3" -source = "git+https://github.com/astral-sh/wiremock-rs?rev=b79b69f62521df9f83a54e866432397562eae789#b79b69f62521df9f83a54e866432397562eae789" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b8b99d4cdbf36b239a9532e31fe4fb8acc38d1897c1761e161550a7dc78e6a" dependencies = [ "assert-json-diff", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index 4269c6cfa..b2d9c0f8a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -189,7 +189,7 @@ windows-core = { version = "0.59.0" } windows-registry = { version = "0.5.0" } windows-result = { version = "0.3.0" } windows-sys = { version = "0.59.0", features = ["Win32_Foundation", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Ioctl", "Win32_System_IO", "Win32_System_Registry"] } -wiremock = { git = "https://github.com/astral-sh/wiremock-rs", rev = "b79b69f62521df9f83a54e866432397562eae789" } +wiremock = { version = "0.6.4" } xz2 = { version = "0.1.7" } zip = { version = "2.2.3", default-features = false, features = ["deflate", "zstd", "bzip2", "lzma", "xz"] } From 093e9d6ff0eef171f59f5a00a14baef6f8ed545c Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 24 Jun 2025 11:15:18 -0400 Subject: [PATCH 077/185] Add `"python-eol"` feature to Sphinx tests (#14241) ## Summary Closes https://github.com/astral-sh/uv/issues/14228. --- crates/uv/tests/it/sync.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index 9da2fb62a..1ce892050 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -355,6 +355,7 @@ fn mixed_requires_python() -> Result<()> { /// Ensure that group requires-python solves an actual problem #[test] #[cfg(not(windows))] +#[cfg(feature = "python-eol")] fn group_requires_python_useful_defaults() -> Result<()> { let context = TestContext::new_with_versions(&["3.8", "3.9"]); @@ -499,6 +500,7 @@ fn group_requires_python_useful_defaults() -> Result<()> { /// Ensure that group requires-python solves an actual problem #[test] #[cfg(not(windows))] +#[cfg(feature = "python-eol")] fn group_requires_python_useful_non_defaults() -> Result<()> { let context = TestContext::new_with_versions(&["3.8", "3.9"]); From f20659e1cededf019777b5aa76ccea908c060abe Mon Sep 17 00:00:00 2001 From: konsti Date: Tue, 24 Jun 2025 17:53:10 +0200 Subject: [PATCH 078/185] Don't log GitHub fast path usage if it's cached (#14235) Don't log that we resolved a reference through the GitHub fast path if we didn't use GitHub at all but used the cached revision. This avoids stating that the fast path works when it's blocked due to unrelated reasons (e.g. rate limits). --- crates/uv-distribution/src/source/mod.rs | 6 ++++ crates/uv-git/src/resolver.rs | 36 +++++++++++++++--------- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs index 90d77bd90..7be26fece 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -1860,6 +1860,12 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { } }; + // If the URL is already precise, return it. + if self.build_context.git().get_precise(git).is_some() { + debug!("Precise commit already known: {source}"); + return Ok(()); + } + // If this is GitHub URL, attempt to resolve to a precise commit using the GitHub API. if self .build_context diff --git a/crates/uv-git/src/resolver.rs b/crates/uv-git/src/resolver.rs index d404390f3..0881910b2 100644 --- a/crates/uv-git/src/resolver.rs +++ b/crates/uv-git/src/resolver.rs @@ -46,6 +46,21 @@ impl GitResolver { self.0.get(reference) } + pub fn get_precise(&self, url: &GitUrl) -> Option { + // If the URL is already precise, return it. + if let Some(precise) = url.precise() { + return Some(precise); + } + + // If we know the precise commit already, return it. + let reference = RepositoryReference::from(url); + if let Some(precise) = self.get(&reference) { + return Some(*precise); + } + + None + } + /// Resolve a Git URL to a specific commit without performing any Git operations. /// /// Returns a [`GitOid`] if the URL has already been resolved (i.e., is available in the cache), @@ -59,18 +74,11 @@ impl GitResolver { return Ok(None); } - let reference = RepositoryReference::from(url); - - // If the URL is already precise, return it. - if let Some(precise) = url.precise() { + // If the URL is already precise or we know the precise commit, return it. + if let Some(precise) = self.get_precise(url) { return Ok(Some(precise)); } - // If we know the precise commit already, return it. - if let Some(precise) = self.get(&reference) { - return Ok(Some(*precise)); - } - // If the URL is a GitHub URL, attempt to resolve it via the GitHub API. let Some(GitHubRepository { owner, repo }) = GitHubRepository::parse(url.repository()) else { @@ -80,10 +88,10 @@ impl GitResolver { // Determine the Git reference. let rev = url.reference().as_rev(); - let url = format!("https://api.github.com/repos/{owner}/{repo}/commits/{rev}"); + let github_api_url = format!("https://api.github.com/repos/{owner}/{repo}/commits/{rev}"); - debug!("Querying GitHub for commit at: {url}"); - let mut request = client.get(&url); + debug!("Querying GitHub for commit at: {github_api_url}"); + let mut request = client.get(&github_api_url); request = request.header("Accept", "application/vnd.github.3.sha"); request = request.header( "User-Agent", @@ -95,7 +103,7 @@ impl GitResolver { // Returns a 404 if the repository does not exist, and a 422 if GitHub is unable to // resolve the requested rev. debug!( - "GitHub API request failed for: {url} ({})", + "GitHub API request failed for: {github_api_url} ({})", response.status() ); return Ok(None); @@ -108,7 +116,7 @@ impl GitResolver { // Insert the resolved URL into the in-memory cache. This ensures that subsequent fetches // resolve to the same precise commit. - self.insert(reference, precise); + self.insert(RepositoryReference::from(url), precise); Ok(Some(precise)) } From 606633d35f4167bc1904de5186db196a343432bb Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 24 Jun 2025 11:54:12 -0400 Subject: [PATCH 079/185] Remove wheel filename benchmark (#14240) ## Summary This flakes often and we don't really need it to be monitored continuously. We can always revive it from Git later. Closes https://github.com/astral-sh/uv/issues/13952. --- Cargo.lock | 1 - crates/uv-bench/Cargo.toml | 6 - .../uv-bench/benches/distribution_filename.rs | 168 ------------------ 3 files changed, 175 deletions(-) delete mode 100644 crates/uv-bench/benches/distribution_filename.rs diff --git a/Cargo.lock b/Cargo.lock index 0680e0c28..ce0325b9e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4719,7 +4719,6 @@ dependencies = [ "uv-configuration", "uv-dispatch", "uv-distribution", - "uv-distribution-filename", "uv-distribution-types", "uv-extract", "uv-install-wheel", diff --git a/crates/uv-bench/Cargo.toml b/crates/uv-bench/Cargo.toml index 65ce78731..5d55fafd7 100644 --- a/crates/uv-bench/Cargo.toml +++ b/crates/uv-bench/Cargo.toml @@ -18,11 +18,6 @@ workspace = true doctest = false bench = false -[[bench]] -name = "distribution-filename" -path = "benches/distribution_filename.rs" -harness = false - [[bench]] name = "uv" path = "benches/uv.rs" @@ -34,7 +29,6 @@ uv-client = { workspace = true } uv-configuration = { workspace = true } uv-dispatch = { workspace = true } uv-distribution = { workspace = true } -uv-distribution-filename = { workspace = true } uv-distribution-types = { workspace = true } uv-extract = { workspace = true, optional = true } uv-install-wheel = { workspace = true } diff --git a/crates/uv-bench/benches/distribution_filename.rs b/crates/uv-bench/benches/distribution_filename.rs deleted file mode 100644 index 99d72cf05..000000000 --- a/crates/uv-bench/benches/distribution_filename.rs +++ /dev/null @@ -1,168 +0,0 @@ -use std::str::FromStr; - -use uv_bench::criterion::{ - BenchmarkId, Criterion, Throughput, criterion_group, criterion_main, measurement::WallTime, -}; -use uv_distribution_filename::WheelFilename; -use uv_platform_tags::{AbiTag, LanguageTag, PlatformTag, Tags}; - -/// A set of platform tags extracted from burntsushi's Archlinux workstation. -/// We could just re-create these via `Tags::from_env`, but those might differ -/// depending on the platform. This way, we always use the same data. It also -/// lets us assert tag compatibility regardless of where the benchmarks run. -const PLATFORM_TAGS: &[(&str, &str, &str)] = include!("../inputs/platform_tags.rs"); - -/// A set of wheel names used in the benchmarks below. We pick short and long -/// names, as well as compatible and not-compatibles (with `PLATFORM_TAGS`) -/// names. -/// -/// The tuple is (name, filename, compatible) where `name` is a descriptive -/// name for humans used in the benchmark definition. And `filename` is the -/// actual wheel filename we want to benchmark operation on. And `compatible` -/// indicates whether the tags in the wheel filename are expected to be -/// compatible with the tags in `PLATFORM_TAGS`. -const WHEEL_NAMES: &[(&str, &str, bool)] = &[ - // This tests a case with a very short name that *is* compatible with - // PLATFORM_TAGS. It only uses one tag for each component (one Python - // version, one ABI and one platform). - ( - "flyte-short-compatible", - "ipython-2.1.0-py3-none-any.whl", - true, - ), - // This tests a case with a long name that is *not* compatible. That - // is, all platform tags need to be checked against the tags in the - // wheel filename. This is essentially the worst possible practical - // case. - ( - "flyte-long-incompatible", - "protobuf-3.5.2.post1-cp36-cp36m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", - false, - ), - // This tests a case with a long name that *is* compatible. We - // expect this to be (on average) quicker because the compatibility - // check stops as soon as a positive match is found. (Where as the - // incompatible case needs to check all tags.) - ( - "flyte-long-compatible", - "coverage-6.6.0b1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", - true, - ), -]; - -/// A list of names that are candidates for wheel filenames but will ultimately -/// fail to parse. -const INVALID_WHEEL_NAMES: &[(&str, &str)] = &[ - ("flyte-short-extension", "mock-5.1.0.tar.gz"), - ( - "flyte-long-extension", - "Pillow-5.4.0.dev0-py3.7-macosx-10.13-x86_64.egg", - ), -]; - -/// Benchmarks the construction of platform tags. -/// -/// This only happens ~once per program startup. Originally, construction was -/// trivial. But to speed up `WheelFilename::is_compatible`, we added some -/// extra processing. We thus expect construction to become slower, but we -/// write a benchmark to ensure it is still "reasonable." -fn benchmark_build_platform_tags(c: &mut Criterion) { - let tags: Vec<(LanguageTag, AbiTag, PlatformTag)> = PLATFORM_TAGS - .iter() - .map(|&(py, abi, plat)| { - ( - LanguageTag::from_str(py).unwrap(), - AbiTag::from_str(abi).unwrap(), - PlatformTag::from_str(plat).unwrap(), - ) - }) - .collect(); - - let mut group = c.benchmark_group("build_platform_tags"); - group.bench_function(BenchmarkId::from_parameter("burntsushi-archlinux"), |b| { - b.iter(|| std::hint::black_box(Tags::new(tags.clone()))); - }); - group.finish(); -} - -/// Benchmarks `WheelFilename::from_str`. This has been observed to take some -/// non-trivial time in profiling (although, at time of writing, not as much -/// as tag compatibility). In the process of optimizing tag compatibility, -/// we tweaked wheel filename parsing. This benchmark was therefore added to -/// ensure we didn't regress here. -fn benchmark_wheelname_parsing(c: &mut Criterion) { - let mut group = c.benchmark_group("wheelname_parsing"); - for (name, filename, _) in WHEEL_NAMES.iter().copied() { - let len = u64::try_from(filename.len()).expect("length fits in u64"); - group.throughput(Throughput::Bytes(len)); - group.bench_function(BenchmarkId::from_parameter(name), |b| { - b.iter(|| { - filename - .parse::() - .expect("valid wheel filename"); - }); - }); - } - group.finish(); -} - -/// Benchmarks `WheelFilename::from_str` when it fails. This routine is called -/// on every filename in a package's metadata. A non-trivial portion of which -/// are not wheel filenames. Ensuring that the error path is fast is thus -/// probably a good idea. -fn benchmark_wheelname_parsing_failure(c: &mut Criterion) { - let mut group = c.benchmark_group("wheelname_parsing_failure"); - for (name, filename) in INVALID_WHEEL_NAMES.iter().copied() { - let len = u64::try_from(filename.len()).expect("length fits in u64"); - group.throughput(Throughput::Bytes(len)); - group.bench_function(BenchmarkId::from_parameter(name), |b| { - b.iter(|| { - filename - .parse::() - .expect_err("invalid wheel filename"); - }); - }); - } - group.finish(); -} - -/// Benchmarks the `WheelFilename::is_compatible` routine. This was revealed -/// to be the #1 bottleneck in the resolver. The main issue was that the -/// set of platform tags (generated once) is quite large, and the original -/// implementation did an exhaustive search over each of them for each tag in -/// the wheel filename. -fn benchmark_wheelname_tag_compatibility(c: &mut Criterion) { - let tags: Vec<(LanguageTag, AbiTag, PlatformTag)> = PLATFORM_TAGS - .iter() - .map(|&(py, abi, plat)| { - ( - LanguageTag::from_str(py).unwrap(), - AbiTag::from_str(abi).unwrap(), - PlatformTag::from_str(plat).unwrap(), - ) - }) - .collect(); - let tags = Tags::new(tags); - - let mut group = c.benchmark_group("wheelname_tag_compatibility"); - for (name, filename, expected) in WHEEL_NAMES.iter().copied() { - let wheelname: WheelFilename = filename.parse().expect("valid wheel filename"); - let len = u64::try_from(filename.len()).expect("length fits in u64"); - group.throughput(Throughput::Bytes(len)); - group.bench_function(BenchmarkId::from_parameter(name), |b| { - b.iter(|| { - assert_eq!(expected, wheelname.is_compatible(&tags)); - }); - }); - } - group.finish(); -} - -criterion_group!( - uv_distribution_filename, - benchmark_build_platform_tags, - benchmark_wheelname_parsing, - benchmark_wheelname_parsing_failure, - benchmark_wheelname_tag_compatibility, -); -criterion_main!(uv_distribution_filename); From 61265b0c1401bd483ea3c04b9304ec36d98c29cd Mon Sep 17 00:00:00 2001 From: dmitry-bychkov Date: Tue, 24 Jun 2025 18:56:36 +0300 Subject: [PATCH 080/185] Add a link to PyPI FAQ to clarify what per-project token is. (#14242) ## Summary This change adds a link to PyPI FAQ about API tokens on the package publishing guide page. To me it wasn't clear what are meant in this section of the docs and it required a little bit of research. Adding explicit link might help beginners. Co-authored-by: Dmitry Bychkov --- docs/guides/package.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guides/package.md b/docs/guides/package.md index 0914d5750..ce5bae7f9 100644 --- a/docs/guides/package.md +++ b/docs/guides/package.md @@ -31,8 +31,8 @@ the effect of declaring a build system in the This setting makes PyPI reject your uploaded package from publishing. It does not affect security or privacy settings on alternative registries. - We also recommend only generating per-project tokens: Without a PyPI token matching the project, - it can't be accidentally published. + We also recommend only generating [per-project PyPI API tokens](https://pypi.org/help/#apitoken): + Without a PyPI token matching the project, it can't be accidentally published. ## Building your package From fe11ceedfa716bdd1f93fd1886318d4cf798e311 Mon Sep 17 00:00:00 2001 From: Christopher Tee Date: Tue, 24 Jun 2025 15:11:41 -0400 Subject: [PATCH 081/185] Skip GitHub fast path when rate-limited (#13033) --- crates/uv-git/src/git.rs | 17 ++++++- crates/uv-git/src/lib.rs | 1 + crates/uv-git/src/rate_limit.rs | 37 ++++++++++++++ crates/uv-git/src/resolver.rs | 24 +++++++-- crates/uv-static/src/env_vars.rs | 4 ++ crates/uv/tests/it/edit.rs | 82 +++++++++++++++++++++++++++++++ crates/uv/tests/it/pip_install.rs | 58 ++++++++++++++++++++++ 7 files changed, 219 insertions(+), 4 deletions(-) create mode 100644 crates/uv-git/src/rate_limit.rs diff --git a/crates/uv-git/src/git.rs b/crates/uv-git/src/git.rs index 298c205ba..4ee4c2670 100644 --- a/crates/uv-git/src/git.rs +++ b/crates/uv-git/src/git.rs @@ -20,6 +20,8 @@ use uv_redacted::DisplaySafeUrl; use uv_static::EnvVars; use uv_version::version; +use crate::rate_limit::{GITHUB_RATE_LIMIT_STATUS, is_github_rate_limited}; + /// A file indicates that if present, `git reset` has been done and a repo /// checkout is ready to go. See [`GitCheckout::reset`] for why we need this. const CHECKOUT_READY_LOCK: &str = ".ok"; @@ -787,7 +789,15 @@ fn github_fast_path( } }; - let url = format!("https://api.github.com/repos/{owner}/{repo}/commits/{github_branch_name}"); + // Check if we're rate-limited by GitHub before determining the FastPathRev + if GITHUB_RATE_LIMIT_STATUS.is_active() { + debug!("Skipping GitHub fast path attempt for: {url} (rate-limited)"); + return Ok(FastPathRev::Indeterminate); + } + + let base_url = std::env::var(EnvVars::UV_GITHUB_FAST_PATH_URL) + .unwrap_or("https://api.github.com/repos".to_owned()); + let url = format!("{base_url}/{owner}/{repo}/commits/{github_branch_name}"); let runtime = tokio::runtime::Builder::new_current_thread() .enable_all() @@ -807,6 +817,11 @@ fn github_fast_path( let response = request.send().await?; + if is_github_rate_limited(&response) { + // Mark that we are being rate-limited by GitHub + GITHUB_RATE_LIMIT_STATUS.activate(); + } + // GitHub returns a 404 if the repository does not exist, and a 422 if it exists but GitHub // is unable to resolve the requested revision. response.error_for_status_ref()?; diff --git a/crates/uv-git/src/lib.rs b/crates/uv-git/src/lib.rs index ef23e58c2..716eb7538 100644 --- a/crates/uv-git/src/lib.rs +++ b/crates/uv-git/src/lib.rs @@ -7,5 +7,6 @@ pub use crate::source::{Fetch, GitSource, Reporter}; mod credentials; mod git; +mod rate_limit; mod resolver; mod source; diff --git a/crates/uv-git/src/rate_limit.rs b/crates/uv-git/src/rate_limit.rs new file mode 100644 index 000000000..4d277e652 --- /dev/null +++ b/crates/uv-git/src/rate_limit.rs @@ -0,0 +1,37 @@ +use reqwest::{Response, StatusCode}; +use std::sync::atomic::{AtomicBool, Ordering}; + +/// A global state on whether we are being rate-limited by GitHub's REST API. +/// If we are, avoid "fast-path" attempts. +pub(crate) static GITHUB_RATE_LIMIT_STATUS: GitHubRateLimitStatus = GitHubRateLimitStatus::new(); + +/// GitHub REST API rate limit status tracker. +/// +/// ## Assumptions +/// +/// The rate limit timeout duration is much longer than the runtime of a `uv` command. +/// And so we do not need to invalidate this state based on `x-ratelimit-reset`. +#[derive(Debug)] +pub(crate) struct GitHubRateLimitStatus(AtomicBool); + +impl GitHubRateLimitStatus { + const fn new() -> Self { + Self(AtomicBool::new(false)) + } + + pub(crate) fn activate(&self) { + self.0.store(true, Ordering::Relaxed); + } + + pub(crate) fn is_active(&self) -> bool { + self.0.load(Ordering::Relaxed) + } +} + +/// Determine if GitHub is applying rate-limiting based on the response +pub(crate) fn is_github_rate_limited(response: &Response) -> bool { + // HTTP 403 and 429 are possible status codes in the event of a primary or secondary rate limit. + // Source: https://docs.github.com/en/rest/using-the-rest-api/troubleshooting-the-rest-api?apiVersion=2022-11-28#rate-limit-errors + let status_code = response.status(); + status_code == StatusCode::FORBIDDEN || status_code == StatusCode::TOO_MANY_REQUESTS +} diff --git a/crates/uv-git/src/resolver.rs b/crates/uv-git/src/resolver.rs index 0881910b2..3c12fc589 100644 --- a/crates/uv-git/src/resolver.rs +++ b/crates/uv-git/src/resolver.rs @@ -15,7 +15,10 @@ use uv_git_types::{GitHubRepository, GitOid, GitReference, GitUrl}; use uv_static::EnvVars; use uv_version::version; -use crate::{Fetch, GitSource, Reporter}; +use crate::{ + Fetch, GitSource, Reporter, + rate_limit::{GITHUB_RATE_LIMIT_STATUS, is_github_rate_limited}, +}; #[derive(Debug, thiserror::Error)] pub enum GitResolverError { @@ -85,10 +88,18 @@ impl GitResolver { return Ok(None); }; + // Check if we're rate-limited by GitHub, before determining the Git reference + if GITHUB_RATE_LIMIT_STATUS.is_active() { + debug!("Rate-limited by GitHub. Skipping GitHub fast path attempt for: {url}"); + return Ok(None); + } + // Determine the Git reference. let rev = url.reference().as_rev(); - let github_api_url = format!("https://api.github.com/repos/{owner}/{repo}/commits/{rev}"); + let github_api_base_url = std::env::var(EnvVars::UV_GITHUB_FAST_PATH_URL) + .unwrap_or("https://api.github.com/repos".to_owned()); + let github_api_url = format!("{github_api_base_url}/{owner}/{repo}/commits/{rev}"); debug!("Querying GitHub for commit at: {github_api_url}"); let mut request = client.get(&github_api_url); @@ -99,13 +110,20 @@ impl GitResolver { ); let response = request.send().await?; - if !response.status().is_success() { + let status = response.status(); + if !status.is_success() { // Returns a 404 if the repository does not exist, and a 422 if GitHub is unable to // resolve the requested rev. debug!( "GitHub API request failed for: {github_api_url} ({})", response.status() ); + + if is_github_rate_limited(&response) { + // Mark that we are being rate-limited by GitHub + GITHUB_RATE_LIMIT_STATUS.activate(); + } + return Ok(None); } diff --git a/crates/uv-static/src/env_vars.rs b/crates/uv-static/src/env_vars.rs index 8193b6aec..4a44579d7 100644 --- a/crates/uv-static/src/env_vars.rs +++ b/crates/uv-static/src/env_vars.rs @@ -667,6 +667,10 @@ impl EnvVars { #[attr_hidden] pub const UV_TEST_INDEX_URL: &'static str = "UV_TEST_INDEX_URL"; + /// Used to set the GitHub fast-path url for tests. + #[attr_hidden] + pub const UV_GITHUB_FAST_PATH_URL: &'static str = "UV_GITHUB_FAST_PATH_URL"; + /// Hide progress messages with non-deterministic order in tests. #[attr_hidden] pub const UV_TEST_NO_CLI_PROGRESS: &'static str = "UV_TEST_NO_CLI_PROGRESS"; diff --git a/crates/uv/tests/it/edit.rs b/crates/uv/tests/it/edit.rs index d117b1e8c..6bdaec17b 100644 --- a/crates/uv/tests/it/edit.rs +++ b/crates/uv/tests/it/edit.rs @@ -494,6 +494,88 @@ fn add_git_private_raw() -> Result<()> { Ok(()) } +#[tokio::test] +#[cfg(feature = "git")] +async fn add_git_private_rate_limited_by_github_rest_api_403_response() -> Result<()> { + let context = TestContext::new("3.12"); + let token = decode_token(READ_ONLY_GITHUB_TOKEN); + + let server = MockServer::start().await; + Mock::given(method("GET")) + .respond_with(ResponseTemplate::new(403)) + .expect(1) + .mount(&server) + .await; + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! {r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + "#})?; + + uv_snapshot!(context.filters(), context + .add() + .arg(format!("uv-private-pypackage @ git+https://{token}@github.com/astral-test/uv-private-pypackage")) + .env("UV_GITHUB_FAST_PATH_URL", server.uri()), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + uv-private-pypackage==0.1.0 (from git+https://github.com/astral-test/uv-private-pypackage@d780faf0ac91257d4d5a4f0c5a0e4509608c0071) + "); + + Ok(()) +} + +#[tokio::test] +#[cfg(feature = "git")] +async fn add_git_private_rate_limited_by_github_rest_api_429_response() -> Result<()> { + use uv_client::DEFAULT_RETRIES; + + let context = TestContext::new("3.12"); + let token = decode_token(READ_ONLY_GITHUB_TOKEN); + + let server = MockServer::start().await; + Mock::given(method("GET")) + .respond_with(ResponseTemplate::new(429)) + .expect(1 + u64::from(DEFAULT_RETRIES)) // Middleware retries on 429 by default + .mount(&server) + .await; + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! {r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + "#})?; + + uv_snapshot!(context.filters(), context + .add() + .arg(format!("uv-private-pypackage @ git+https://{token}@github.com/astral-test/uv-private-pypackage")) + .env("UV_GITHUB_FAST_PATH_URL", server.uri()), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + uv-private-pypackage==0.1.0 (from git+https://github.com/astral-test/uv-private-pypackage@d780faf0ac91257d4d5a4f0c5a0e4509608c0071) + "); + + Ok(()) +} + #[test] #[cfg(feature = "git")] fn add_git_error() -> Result<()> { diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index e0876b23c..604b8db15 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -2066,6 +2066,64 @@ fn install_git_public_https_missing_branch_or_tag() { "###); } +#[tokio::test] +#[cfg(feature = "git")] +async fn install_git_public_rate_limited_by_github_rest_api_403_response() { + let context = TestContext::new("3.12"); + + let server = MockServer::start().await; + Mock::given(method("GET")) + .respond_with(ResponseTemplate::new(403)) + .expect(1) + .mount(&server) + .await; + + uv_snapshot!(context.filters(), context + .pip_install() + .arg("uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage") + .env("UV_GITHUB_FAST_PATH_URL", server.uri()), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + uv-public-pypackage==0.1.0 (from git+https://github.com/astral-test/uv-public-pypackage@b270df1a2fb5d012294e9aaf05e7e0bab1e6a389) + "); +} + +#[tokio::test] +#[cfg(feature = "git")] +async fn install_git_public_rate_limited_by_github_rest_api_429_response() { + use uv_client::DEFAULT_RETRIES; + + let context = TestContext::new("3.12"); + + let server = MockServer::start().await; + Mock::given(method("GET")) + .respond_with(ResponseTemplate::new(429)) + .expect(1 + u64::from(DEFAULT_RETRIES)) // Middleware retries on 429 by default + .mount(&server) + .await; + + uv_snapshot!(context.filters(), context + .pip_install() + .arg("uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage") + .env("UV_GITHUB_FAST_PATH_URL", server.uri()), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + uv-public-pypackage==0.1.0 (from git+https://github.com/astral-test/uv-public-pypackage@b270df1a2fb5d012294e9aaf05e7e0bab1e6a389) + "); +} + /// Install a package from a public GitHub repository at a ref that does not exist #[test] #[cfg(feature = "git")] From 9fba7a4768a5931df97af158cc6e81cf3d06ed3a Mon Sep 17 00:00:00 2001 From: Christopher Tee Date: Tue, 24 Jun 2025 15:30:26 -0400 Subject: [PATCH 082/185] Consistently use `Ordering::Relaxed` for standalone atomic use cases (#14190) --- crates/uv-configuration/src/threading.rs | 2 +- crates/uv-warnings/src/lib.rs | 8 ++++---- crates/uv/src/lib.rs | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/uv-configuration/src/threading.rs b/crates/uv-configuration/src/threading.rs index 58b6190a6..2f70b5d81 100644 --- a/crates/uv-configuration/src/threading.rs +++ b/crates/uv-configuration/src/threading.rs @@ -62,7 +62,7 @@ pub static RAYON_PARALLELISM: AtomicUsize = AtomicUsize::new(0); /// `LazyLock::force(&RAYON_INITIALIZE)`. pub static RAYON_INITIALIZE: LazyLock<()> = LazyLock::new(|| { rayon::ThreadPoolBuilder::new() - .num_threads(RAYON_PARALLELISM.load(Ordering::SeqCst)) + .num_threads(RAYON_PARALLELISM.load(Ordering::Relaxed)) .stack_size(min_stack_size()) .build_global() .expect("failed to initialize global rayon pool"); diff --git a/crates/uv-warnings/src/lib.rs b/crates/uv-warnings/src/lib.rs index 5f2287cac..2b664be8d 100644 --- a/crates/uv-warnings/src/lib.rs +++ b/crates/uv-warnings/src/lib.rs @@ -13,12 +13,12 @@ pub static ENABLED: AtomicBool = AtomicBool::new(false); /// Enable user-facing warnings. pub fn enable() { - ENABLED.store(true, std::sync::atomic::Ordering::SeqCst); + ENABLED.store(true, std::sync::atomic::Ordering::Relaxed); } /// Disable user-facing warnings. pub fn disable() { - ENABLED.store(false, std::sync::atomic::Ordering::SeqCst); + ENABLED.store(false, std::sync::atomic::Ordering::Relaxed); } /// Warn a user, if warnings are enabled. @@ -28,7 +28,7 @@ macro_rules! warn_user { use $crate::anstream::eprintln; use $crate::owo_colors::OwoColorize; - if $crate::ENABLED.load(std::sync::atomic::Ordering::SeqCst) { + if $crate::ENABLED.load(std::sync::atomic::Ordering::Relaxed) { let message = format!("{}", format_args!($($arg)*)); let formatted = message.bold(); eprintln!("{}{} {formatted}", "warning".yellow().bold(), ":".bold()); @@ -46,7 +46,7 @@ macro_rules! warn_user_once { use $crate::anstream::eprintln; use $crate::owo_colors::OwoColorize; - if $crate::ENABLED.load(std::sync::atomic::Ordering::SeqCst) { + if $crate::ENABLED.load(std::sync::atomic::Ordering::Relaxed) { if let Ok(mut states) = $crate::WARNINGS.lock() { let message = format!("{}", format_args!($($arg)*)); if states.insert(message.clone()) { diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index fd2e28fae..ab4aee9e9 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -400,7 +400,7 @@ async fn run(mut cli: Cli) -> Result { }))?; // Don't initialize the rayon threadpool yet, this is too costly when we're doing a noop sync. - uv_configuration::RAYON_PARALLELISM.store(globals.concurrency.installs, Ordering::SeqCst); + uv_configuration::RAYON_PARALLELISM.store(globals.concurrency.installs, Ordering::Relaxed); debug!("uv {}", uv_cli::version::uv_self_version()); From ac788d7cdeaf8b6de9d06971d19ae95fc74e0b5d Mon Sep 17 00:00:00 2001 From: ya7010 <47286750+ya7010@users.noreply.github.com> Date: Wed, 25 Jun 2025 04:43:31 +0900 Subject: [PATCH 083/185] Update schemars 1.0.0 (#13693) ## Summary Update [schemars 0.9.0](https://github.com/GREsau/schemars/releases/tag/v0.9.0) There are differences in the generated JSON Schema and I will [contact the author](https://github.com/GREsau/schemars/issues/407). ## Test Plan --------- Co-authored-by: konstin --- Cargo.lock | 9 +- Cargo.toml | 2 +- .../uv-configuration/src/name_specifiers.rs | 30 +- .../uv-configuration/src/required_version.rs | 21 +- crates/uv-configuration/src/trusted_host.rs | 21 +- crates/uv-dev/src/generate_json_schema.rs | 7 +- crates/uv-distribution-types/src/index_url.rs | 19 +- crates/uv-distribution-types/src/pip_index.rs | 10 +- .../src/status_code_strategy.rs | 20 +- crates/uv-fs/src/path.rs | 6 +- crates/uv-pep508/Cargo.toml | 2 +- crates/uv-pep508/src/lib.rs | 22 +- crates/uv-pep508/src/marker/tree.rs | 24 +- crates/uv-pypi-types/src/conflicts.rs | 10 +- crates/uv-pypi-types/src/identifier.rs | 26 +- crates/uv-python/src/python_version.rs | 27 +- crates/uv-resolver/src/exclude_newer.rs | 28 +- crates/uv-settings/src/settings.rs | 1 + crates/uv-small-str/src/lib.rs | 10 +- crates/uv-workspace/src/pyproject.rs | 9 +- uv.schema.json | 1033 +++++++---------- 21 files changed, 524 insertions(+), 813 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ce0325b9e..2e1957e9f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3405,11 +3405,12 @@ dependencies = [ [[package]] name = "schemars" -version = "0.8.22" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +checksum = "febc07c7e70b5db4f023485653c754d76e1bbe8d9dbfa20193ce13da9f9633f4" dependencies = [ "dyn-clone", + "ref-cast", "schemars_derive", "serde", "serde_json", @@ -3418,9 +3419,9 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "0.8.22" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +checksum = "c1eeedaab7b1e1d09b5b4661121f4d27f9e7487089b0117833ccd7a882ee1ecc" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index b2d9c0f8a..5f3cdf40c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -151,7 +151,7 @@ rust-netrc = { version = "0.1.2" } rustc-hash = { version = "2.0.0" } rustix = { version = "1.0.0", default-features = false, features = ["fs", "std"] } same-file = { version = "1.0.6" } -schemars = { version = "0.8.21", features = ["url"] } +schemars = { version = "1.0.0", features = ["url2"] } seahash = { version = "4.1.0" } self-replace = { version = "1.5.0" } serde = { version = "1.0.210", features = ["derive", "rc"] } diff --git a/crates/uv-configuration/src/name_specifiers.rs b/crates/uv-configuration/src/name_specifiers.rs index 5ff209948..1fd25ea0b 100644 --- a/crates/uv-configuration/src/name_specifiers.rs +++ b/crates/uv-configuration/src/name_specifiers.rs @@ -1,4 +1,4 @@ -use std::str::FromStr; +use std::{borrow::Cow, str::FromStr}; use uv_pep508::PackageName; @@ -63,28 +63,16 @@ impl<'de> serde::Deserialize<'de> for PackageNameSpecifier { #[cfg(feature = "schemars")] impl schemars::JsonSchema for PackageNameSpecifier { - fn schema_name() -> String { - "PackageNameSpecifier".to_string() + fn schema_name() -> Cow<'static, str> { + Cow::Borrowed("PackageNameSpecifier") } - fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { - schemars::schema::SchemaObject { - instance_type: Some(schemars::schema::InstanceType::String.into()), - string: Some(Box::new(schemars::schema::StringValidation { - // See: https://packaging.python.org/en/latest/specifications/name-normalization/#name-format - pattern: Some( - r"^(:none:|:all:|([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9]))$" - .to_string(), - ), - ..schemars::schema::StringValidation::default() - })), - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some("The name of a package, or `:all:` or `:none:` to select or omit all packages, respectively.".to_string()), - ..schemars::schema::Metadata::default() - })), - ..schemars::schema::SchemaObject::default() - } - .into() + fn json_schema(_gen: &mut schemars::generate::SchemaGenerator) -> schemars::Schema { + schemars::json_schema!({ + "type": "string", + "pattern": r"^(:none:|:all:|([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9]))$", + "description": "The name of a package, or `:all:` or `:none:` to select or omit all packages, respectively.", + }) } } diff --git a/crates/uv-configuration/src/required_version.rs b/crates/uv-configuration/src/required_version.rs index a0138a46e..7135dfdab 100644 --- a/crates/uv-configuration/src/required_version.rs +++ b/crates/uv-configuration/src/required_version.rs @@ -1,5 +1,5 @@ -use std::fmt::Formatter; use std::str::FromStr; +use std::{borrow::Cow, fmt::Formatter}; use uv_pep440::{Version, VersionSpecifier, VersionSpecifiers, VersionSpecifiersParseError}; @@ -36,20 +36,15 @@ impl FromStr for RequiredVersion { #[cfg(feature = "schemars")] impl schemars::JsonSchema for RequiredVersion { - fn schema_name() -> String { - String::from("RequiredVersion") + fn schema_name() -> Cow<'static, str> { + Cow::Borrowed("RequiredVersion") } - fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { - schemars::schema::SchemaObject { - instance_type: Some(schemars::schema::InstanceType::String.into()), - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some("A version specifier, e.g. `>=0.5.0` or `==0.5.0`.".to_string()), - ..schemars::schema::Metadata::default() - })), - ..schemars::schema::SchemaObject::default() - } - .into() + fn json_schema(_generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema { + schemars::json_schema!({ + "type": "string", + "description": "A version specifier, e.g. `>=0.5.0` or `==0.5.0`." + }) } } diff --git a/crates/uv-configuration/src/trusted_host.rs b/crates/uv-configuration/src/trusted_host.rs index 64fb14169..9f3efb6fc 100644 --- a/crates/uv-configuration/src/trusted_host.rs +++ b/crates/uv-configuration/src/trusted_host.rs @@ -1,5 +1,5 @@ use serde::{Deserialize, Deserializer}; -use std::str::FromStr; +use std::{borrow::Cow, str::FromStr}; use url::Url; /// A host specification (wildcard, or host, with optional scheme and/or port) for which @@ -143,20 +143,15 @@ impl std::fmt::Display for TrustedHost { #[cfg(feature = "schemars")] impl schemars::JsonSchema for TrustedHost { - fn schema_name() -> String { - "TrustedHost".to_string() + fn schema_name() -> Cow<'static, str> { + Cow::Borrowed("TrustedHost") } - fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { - schemars::schema::SchemaObject { - instance_type: Some(schemars::schema::InstanceType::String.into()), - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some("A host or host-port pair.".to_string()), - ..schemars::schema::Metadata::default() - })), - ..schemars::schema::SchemaObject::default() - } - .into() + fn json_schema(_generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema { + schemars::json_schema!({ + "type": "string", + "description": "A host or host-port pair." + }) } } diff --git a/crates/uv-dev/src/generate_json_schema.rs b/crates/uv-dev/src/generate_json_schema.rs index 75465f429..8a4ff47d5 100644 --- a/crates/uv-dev/src/generate_json_schema.rs +++ b/crates/uv-dev/src/generate_json_schema.rs @@ -3,7 +3,7 @@ use std::path::PathBuf; use anstream::println; use anyhow::{Result, bail}; use pretty_assertions::StrComparison; -use schemars::{JsonSchema, schema_for}; +use schemars::JsonSchema; use serde::Deserialize; use uv_settings::Options as SettingsOptions; @@ -91,7 +91,10 @@ const REPLACEMENTS: &[(&str, &str)] = &[ /// Generate the JSON schema for the combined options as a string. fn generate() -> String { - let schema = schema_for!(CombinedOptions); + let settings = schemars::generate::SchemaSettings::draft07(); + let generator = schemars::SchemaGenerator::new(settings); + let schema = generator.into_root_schema_for::(); + let mut output = serde_json::to_string_pretty(&schema).unwrap(); for (value, replacement) in REPLACEMENTS { diff --git a/crates/uv-distribution-types/src/index_url.rs b/crates/uv-distribution-types/src/index_url.rs index a523b4811..9d07c9cbe 100644 --- a/crates/uv-distribution-types/src/index_url.rs +++ b/crates/uv-distribution-types/src/index_url.rs @@ -92,20 +92,15 @@ impl IndexUrl { #[cfg(feature = "schemars")] impl schemars::JsonSchema for IndexUrl { - fn schema_name() -> String { - "IndexUrl".to_string() + fn schema_name() -> Cow<'static, str> { + Cow::Borrowed("IndexUrl") } - fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { - schemars::schema::SchemaObject { - instance_type: Some(schemars::schema::InstanceType::String.into()), - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some("The URL of an index to use for fetching packages (e.g., `https://pypi.org/simple`), or a local path.".to_string()), - ..schemars::schema::Metadata::default() - })), - ..schemars::schema::SchemaObject::default() - } - .into() + fn json_schema(_generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema { + schemars::json_schema!({ + "type": "string", + "description": "The URL of an index to use for fetching packages (e.g., `https://pypi.org/simple`), or a local path." + }) } } diff --git a/crates/uv-distribution-types/src/pip_index.rs b/crates/uv-distribution-types/src/pip_index.rs index 6ce22abd2..888c0df0d 100644 --- a/crates/uv-distribution-types/src/pip_index.rs +++ b/crates/uv-distribution-types/src/pip_index.rs @@ -3,7 +3,7 @@ //! flags set. use serde::{Deserialize, Deserializer, Serialize}; -use std::path::Path; +use std::{borrow::Cow, path::Path}; use crate::{Index, IndexUrl}; @@ -50,14 +50,14 @@ macro_rules! impl_index { #[cfg(feature = "schemars")] impl schemars::JsonSchema for $name { - fn schema_name() -> String { + fn schema_name() -> Cow<'static, str> { IndexUrl::schema_name() } fn json_schema( - r#gen: &mut schemars::r#gen::SchemaGenerator, - ) -> schemars::schema::Schema { - IndexUrl::json_schema(r#gen) + generator: &mut schemars::generate::SchemaGenerator, + ) -> schemars::Schema { + IndexUrl::json_schema(generator) } } }; diff --git a/crates/uv-distribution-types/src/status_code_strategy.rs b/crates/uv-distribution-types/src/status_code_strategy.rs index a2940a23a..709f68be1 100644 --- a/crates/uv-distribution-types/src/status_code_strategy.rs +++ b/crates/uv-distribution-types/src/status_code_strategy.rs @@ -1,4 +1,4 @@ -use std::ops::Deref; +use std::{borrow::Cow, ops::Deref}; use http::StatusCode; use rustc_hash::FxHashSet; @@ -136,17 +136,17 @@ impl<'de> Deserialize<'de> for SerializableStatusCode { #[cfg(feature = "schemars")] impl schemars::JsonSchema for SerializableStatusCode { - fn schema_name() -> String { - "StatusCode".to_string() + fn schema_name() -> Cow<'static, str> { + Cow::Borrowed("StatusCode") } - fn json_schema(r#gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { - let mut schema = r#gen.subschema_for::().into_object(); - schema.metadata().description = Some("HTTP status code (100-599)".to_string()); - schema.number().minimum = Some(100.0); - schema.number().maximum = Some(599.0); - - schema.into() + fn json_schema(_generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema { + schemars::json_schema!({ + "type": "number", + "minimum": 100, + "maximum": 599, + "description": "HTTP status code (100-599)" + }) } } diff --git a/crates/uv-fs/src/path.rs b/crates/uv-fs/src/path.rs index 90d0ce80a..40e579f8e 100644 --- a/crates/uv-fs/src/path.rs +++ b/crates/uv-fs/src/path.rs @@ -330,11 +330,11 @@ pub struct PortablePathBuf(Box); #[cfg(feature = "schemars")] impl schemars::JsonSchema for PortablePathBuf { - fn schema_name() -> String { - PathBuf::schema_name() + fn schema_name() -> Cow<'static, str> { + Cow::Borrowed("PortablePathBuf") } - fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { + fn json_schema(_gen: &mut schemars::generate::SchemaGenerator) -> schemars::Schema { PathBuf::json_schema(_gen) } } diff --git a/crates/uv-pep508/Cargo.toml b/crates/uv-pep508/Cargo.toml index 7494a722d..e9306da00 100644 --- a/crates/uv-pep508/Cargo.toml +++ b/crates/uv-pep508/Cargo.toml @@ -41,7 +41,7 @@ version-ranges = { workspace = true } [dev-dependencies] insta = { version = "1.40.0" } -serde_json = { version = "1.0.128" } +serde_json = { workspace = true } tracing-test = { version = "0.2.5" } [features] diff --git a/crates/uv-pep508/src/lib.rs b/crates/uv-pep508/src/lib.rs index e313db86d..46e2f3039 100644 --- a/crates/uv-pep508/src/lib.rs +++ b/crates/uv-pep508/src/lib.rs @@ -16,6 +16,7 @@ #![warn(missing_docs)] +use std::borrow::Cow; use std::error::Error; use std::fmt::{Debug, Display, Formatter}; use std::path::Path; @@ -334,22 +335,15 @@ impl Reporter for TracingReporter { #[cfg(feature = "schemars")] impl schemars::JsonSchema for Requirement { - fn schema_name() -> String { - "Requirement".to_string() + fn schema_name() -> Cow<'static, str> { + Cow::Borrowed("Requirement") } - fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { - schemars::schema::SchemaObject { - instance_type: Some(schemars::schema::InstanceType::String.into()), - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - "A PEP 508 dependency specifier, e.g., `ruff >= 0.6.0`".to_string(), - ), - ..schemars::schema::Metadata::default() - })), - ..schemars::schema::SchemaObject::default() - } - .into() + fn json_schema(_gen: &mut schemars::generate::SchemaGenerator) -> schemars::Schema { + schemars::json_schema!({ + "type": "string", + "description": "A PEP 508 dependency specifier, e.g., `ruff >= 0.6.0`" + }) } } diff --git a/crates/uv-pep508/src/marker/tree.rs b/crates/uv-pep508/src/marker/tree.rs index 070a24b26..975d4ff10 100644 --- a/crates/uv-pep508/src/marker/tree.rs +++ b/crates/uv-pep508/src/marker/tree.rs @@ -1707,23 +1707,15 @@ impl Display for MarkerTreeContents { #[cfg(feature = "schemars")] impl schemars::JsonSchema for MarkerTree { - fn schema_name() -> String { - "MarkerTree".to_string() + fn schema_name() -> Cow<'static, str> { + Cow::Borrowed("MarkerTree") } - fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { - schemars::schema::SchemaObject { - instance_type: Some(schemars::schema::InstanceType::String.into()), - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - "A PEP 508-compliant marker expression, e.g., `sys_platform == 'Darwin'`" - .to_string(), - ), - ..schemars::schema::Metadata::default() - })), - ..schemars::schema::SchemaObject::default() - } - .into() + fn json_schema(_generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema { + schemars::json_schema!({ + "type": "string", + "description": "A PEP 508-compliant marker expression, e.g., `sys_platform == 'Darwin'`" + }) } } @@ -2515,7 +2507,7 @@ mod test { #[test] fn test_simplification_extra_versus_other() { // Here, the `extra != 'foo'` cannot be simplified out, because - // `extra == 'foo'` can be true even when `extra == 'bar`' is true. + // `extra == 'foo'` can be true even when `extra == 'bar'`' is true. assert_simplifies( r#"extra != "foo" and (extra == "bar" or extra == "baz")"#, "(extra == 'bar' and extra != 'foo') or (extra == 'baz' and extra != 'foo')", diff --git a/crates/uv-pypi-types/src/conflicts.rs b/crates/uv-pypi-types/src/conflicts.rs index 94366bfd2..dd1e96b77 100644 --- a/crates/uv-pypi-types/src/conflicts.rs +++ b/crates/uv-pypi-types/src/conflicts.rs @@ -3,7 +3,7 @@ use petgraph::{ graph::{DiGraph, NodeIndex}, }; use rustc_hash::{FxHashMap, FxHashSet}; -use std::{collections::BTreeSet, hash::Hash, rc::Rc}; +use std::{borrow::Cow, collections::BTreeSet, hash::Hash, rc::Rc}; use uv_normalize::{ExtraName, GroupName, PackageName}; use crate::dependency_groups::{DependencyGroupSpecifier, DependencyGroups}; @@ -638,12 +638,12 @@ pub struct SchemaConflictItem { #[cfg(feature = "schemars")] impl schemars::JsonSchema for SchemaConflictItem { - fn schema_name() -> String { - "SchemaConflictItem".to_string() + fn schema_name() -> Cow<'static, str> { + Cow::Borrowed("SchemaConflictItem") } - fn json_schema(r#gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { - ::json_schema(r#gen) + fn json_schema(generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema { + ::json_schema(generator) } } diff --git a/crates/uv-pypi-types/src/identifier.rs b/crates/uv-pypi-types/src/identifier.rs index b0c78d5b2..972a327ae 100644 --- a/crates/uv-pypi-types/src/identifier.rs +++ b/crates/uv-pypi-types/src/identifier.rs @@ -1,4 +1,5 @@ use serde::{Serialize, Serializer}; +use std::borrow::Cow; use std::fmt::Display; use std::str::FromStr; use thiserror::Error; @@ -99,25 +100,16 @@ impl Serialize for Identifier { #[cfg(feature = "schemars")] impl schemars::JsonSchema for Identifier { - fn schema_name() -> String { - "Identifier".to_string() + fn schema_name() -> Cow<'static, str> { + Cow::Borrowed("Identifier") } - fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { - schemars::schema::SchemaObject { - instance_type: Some(schemars::schema::InstanceType::String.into()), - string: Some(Box::new(schemars::schema::StringValidation { - // Best-effort Unicode support (https://stackoverflow.com/a/68844380/3549270) - pattern: Some(r"^[_\p{Alphabetic}][_0-9\p{Alphabetic}]*$".to_string()), - ..schemars::schema::StringValidation::default() - })), - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some("An identifier in Python".to_string()), - ..schemars::schema::Metadata::default() - })), - ..schemars::schema::SchemaObject::default() - } - .into() + fn json_schema(_generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema { + schemars::json_schema!({ + "type": "string", + "pattern": r"^[_\p{Alphabetic}][_0-9\p{Alphabetic}]*$", + "description": "An identifier in Python" + }) } } diff --git a/crates/uv-python/src/python_version.rs b/crates/uv-python/src/python_version.rs index 39e8e3429..63f50f226 100644 --- a/crates/uv-python/src/python_version.rs +++ b/crates/uv-python/src/python_version.rs @@ -1,3 +1,4 @@ +use std::borrow::Cow; use std::fmt::{Display, Formatter}; use std::ops::Deref; use std::str::FromStr; @@ -65,26 +66,16 @@ impl FromStr for PythonVersion { #[cfg(feature = "schemars")] impl schemars::JsonSchema for PythonVersion { - fn schema_name() -> String { - String::from("PythonVersion") + fn schema_name() -> Cow<'static, str> { + Cow::Borrowed("PythonVersion") } - fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { - schemars::schema::SchemaObject { - instance_type: Some(schemars::schema::InstanceType::String.into()), - string: Some(Box::new(schemars::schema::StringValidation { - pattern: Some(r"^3\.\d+(\.\d+)?$".to_string()), - ..schemars::schema::StringValidation::default() - })), - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - "A Python version specifier, e.g. `3.11` or `3.12.4`.".to_string(), - ), - ..schemars::schema::Metadata::default() - })), - ..schemars::schema::SchemaObject::default() - } - .into() + fn json_schema(_generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema { + schemars::json_schema!({ + "type": "string", + "pattern": r"^3\.\d+(\.\d+)?$", + "description": "A Python version specifier, e.g. `3.11` or `3.12.4`." + }) } } diff --git a/crates/uv-resolver/src/exclude_newer.rs b/crates/uv-resolver/src/exclude_newer.rs index 40ec009f8..c1ac6adb8 100644 --- a/crates/uv-resolver/src/exclude_newer.rs +++ b/crates/uv-resolver/src/exclude_newer.rs @@ -1,4 +1,4 @@ -use std::str::FromStr; +use std::{borrow::Cow, str::FromStr}; use jiff::{Timestamp, ToSpan, tz::TimeZone}; @@ -67,25 +67,15 @@ impl std::fmt::Display for ExcludeNewer { #[cfg(feature = "schemars")] impl schemars::JsonSchema for ExcludeNewer { - fn schema_name() -> String { - "ExcludeNewer".to_string() + fn schema_name() -> Cow<'static, str> { + Cow::Borrowed("ExcludeNewer") } - fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { - schemars::schema::SchemaObject { - instance_type: Some(schemars::schema::InstanceType::String.into()), - string: Some(Box::new(schemars::schema::StringValidation { - pattern: Some( - r"^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(Z|[+-]\d{2}:\d{2}))?$".to_string(), - ), - ..schemars::schema::StringValidation::default() - })), - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some("Exclude distributions uploaded after the given timestamp.\n\nAccepts both RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`) and local dates in the same format (e.g., `2006-12-02`).".to_string()), - ..schemars::schema::Metadata::default() - })), - ..schemars::schema::SchemaObject::default() - } - .into() + fn json_schema(_generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema { + schemars::json_schema!({ + "type": "string", + "pattern": r"^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(Z|[+-]\d{2}:\d{2}))?$", + "description": "Exclude distributions uploaded after the given timestamp.\n\nAccepts both RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`) and local dates in the same format (e.g., `2006-12-02`).", + }) } } diff --git a/crates/uv-settings/src/settings.rs b/crates/uv-settings/src/settings.rs index 2c18fb40a..d80ccce2f 100644 --- a/crates/uv-settings/src/settings.rs +++ b/crates/uv-settings/src/settings.rs @@ -41,6 +41,7 @@ pub(crate) struct Tools { #[derive(Debug, Clone, Default, Deserialize, CombineOptions, OptionsMetadata)] #[serde(from = "OptionsWire", rename_all = "kebab-case")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "schemars", schemars(!from))] pub struct Options { #[serde(flatten)] pub globals: GlobalOptions, diff --git a/crates/uv-small-str/src/lib.rs b/crates/uv-small-str/src/lib.rs index 7395c090a..1524f1b99 100644 --- a/crates/uv-small-str/src/lib.rs +++ b/crates/uv-small-str/src/lib.rs @@ -147,15 +147,15 @@ impl PartialOrd for rkyv::string::ArchivedString { /// An [`schemars::JsonSchema`] implementation for [`SmallString`]. #[cfg(feature = "schemars")] impl schemars::JsonSchema for SmallString { - fn is_referenceable() -> bool { - String::is_referenceable() + fn inline_schema() -> bool { + true } - fn schema_name() -> String { + fn schema_name() -> Cow<'static, str> { String::schema_name() } - fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { - String::json_schema(_gen) + fn json_schema(generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema { + String::json_schema(generator) } } diff --git a/crates/uv-workspace/src/pyproject.rs b/crates/uv-workspace/src/pyproject.rs index 6499aad5d..dabbe33fb 100644 --- a/crates/uv-workspace/src/pyproject.rs +++ b/crates/uv-workspace/src/pyproject.rs @@ -6,6 +6,7 @@ //! //! Then lowers them into a dependency specification. +use std::borrow::Cow; use std::collections::BTreeMap; use std::fmt::Formatter; use std::ops::Deref; @@ -813,12 +814,12 @@ impl<'de> serde::Deserialize<'de> for SerdePattern { #[cfg(feature = "schemars")] impl schemars::JsonSchema for SerdePattern { - fn schema_name() -> String { - ::schema_name() + fn schema_name() -> Cow<'static, str> { + Cow::Borrowed("SerdePattern") } - fn json_schema(r#gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { - ::json_schema(r#gen) + fn json_schema(generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema { + ::json_schema(generator) } } diff --git a/uv.schema.json b/uv.schema.json index 0b9bd6f15..b00463ee9 100644 --- a/uv.schema.json +++ b/uv.schema.json @@ -5,7 +5,7 @@ "type": "object", "properties": { "add-bounds": { - "description": "The default version specifier when adding a dependency.\n\nWhen adding a dependency to the project, if no constraint or URL is provided, a constraint is added based on the latest compatible version of the package. By default, a lower bound constraint is used, e.g., `>=1.2.3`.\n\nWhen `--frozen` is provided, no resolution is performed, and dependencies are always added without constraints.\n\nThis option is in preview and may change in any future release.", + "description": "The default version specifier when adding a dependency.\n\nWhen adding a dependency to the project, if no constraint or URL is provided, a constraint\nis added based on the latest compatible version of the package. By default, a lower bound\nconstraint is used, e.g., `>=1.2.3`.\n\nWhen `--frozen` is provided, no resolution is performed, and dependencies are always added\nwithout constraints.\n\nThis option is in preview and may change in any future release.", "anyOf": [ { "$ref": "#/definitions/AddBoundsKind" @@ -16,7 +16,7 @@ ] }, "allow-insecure-host": { - "description": "Allow insecure connections to host.\n\nExpects to receive either a hostname (e.g., `localhost`), a host-port pair (e.g., `localhost:8080`), or a URL (e.g., `https://localhost`).\n\nWARNING: Hosts included in this list will not be verified against the system's certificate store. Only use `--allow-insecure-host` in a secure network with verified sources, as it bypasses SSL verification and could expose you to MITM attacks.", + "description": "Allow insecure connections to host.\n\nExpects to receive either a hostname (e.g., `localhost`), a host-port pair (e.g.,\n`localhost:8080`), or a URL (e.g., `https://localhost`).\n\nWARNING: Hosts included in this list will not be verified against the system's certificate\nstore. Only use `--allow-insecure-host` in a secure network with verified sources, as it\nbypasses SSL verification and could expose you to MITM attacks.", "type": [ "array", "null" @@ -26,7 +26,7 @@ } }, "build-backend": { - "description": "Configuration for the uv build backend.\n\nNote that those settings only apply when using the `uv_build` backend, other build backends (such as hatchling) have their own configuration.", + "description": "Configuration for the uv build backend.\n\nNote that those settings only apply when using the `uv_build` backend, other build backends\n(such as hatchling) have their own configuration.", "anyOf": [ { "$ref": "#/definitions/BuildBackendSettings" @@ -47,14 +47,14 @@ } }, "cache-dir": { - "description": "Path to the cache directory.\n\nDefaults to `$XDG_CACHE_HOME/uv` or `$HOME/.cache/uv` on Linux and macOS, and `%LOCALAPPDATA%\\uv\\cache` on Windows.", + "description": "Path to the cache directory.\n\nDefaults to `$XDG_CACHE_HOME/uv` or `$HOME/.cache/uv` on Linux and macOS, and\n`%LOCALAPPDATA%\\uv\\cache` on Windows.", "type": [ "string", "null" ] }, "cache-keys": { - "description": "The keys to consider when caching builds for the project.\n\nCache keys enable you to specify the files or directories that should trigger a rebuild when modified. By default, uv will rebuild a project whenever the `pyproject.toml`, `setup.py`, or `setup.cfg` files in the project directory are modified, or if a `src` directory is added or removed, i.e.:\n\n```toml cache-keys = [{ file = \"pyproject.toml\" }, { file = \"setup.py\" }, { file = \"setup.cfg\" }, { dir = \"src\" }] ```\n\nAs an example: if a project uses dynamic metadata to read its dependencies from a `requirements.txt` file, you can specify `cache-keys = [{ file = \"requirements.txt\" }, { file = \"pyproject.toml\" }]` to ensure that the project is rebuilt whenever the `requirements.txt` file is modified (in addition to watching the `pyproject.toml`).\n\nGlobs are supported, following the syntax of the [`glob`](https://docs.rs/glob/0.3.1/glob/struct.Pattern.html) crate. For example, to invalidate the cache whenever a `.toml` file in the project directory or any of its subdirectories is modified, you can specify `cache-keys = [{ file = \"**/*.toml\" }]`. Note that the use of globs can be expensive, as uv may need to walk the filesystem to determine whether any files have changed.\n\nCache keys can also include version control information. For example, if a project uses `setuptools_scm` to read its version from a Git commit, you can specify `cache-keys = [{ git = { commit = true }, { file = \"pyproject.toml\" }]` to include the current Git commit hash in the cache key (in addition to the `pyproject.toml`). Git tags are also supported via `cache-keys = [{ git = { commit = true, tags = true } }]`.\n\nCache keys can also include environment variables. For example, if a project relies on `MACOSX_DEPLOYMENT_TARGET` or other environment variables to determine its behavior, you can specify `cache-keys = [{ env = \"MACOSX_DEPLOYMENT_TARGET\" }]` to invalidate the cache whenever the environment variable changes.\n\nCache keys only affect the project defined by the `pyproject.toml` in which they're specified (as opposed to, e.g., affecting all members in a workspace), and all paths and globs are interpreted as relative to the project directory.", + "description": "The keys to consider when caching builds for the project.\n\nCache keys enable you to specify the files or directories that should trigger a rebuild when\nmodified. By default, uv will rebuild a project whenever the `pyproject.toml`, `setup.py`,\nor `setup.cfg` files in the project directory are modified, or if a `src` directory is\nadded or removed, i.e.:\n\n```toml\ncache-keys = [{ file = \"pyproject.toml\" }, { file = \"setup.py\" }, { file = \"setup.cfg\" }, { dir = \"src\" }]\n```\n\nAs an example: if a project uses dynamic metadata to read its dependencies from a\n`requirements.txt` file, you can specify `cache-keys = [{ file = \"requirements.txt\" }, { file = \"pyproject.toml\" }]`\nto ensure that the project is rebuilt whenever the `requirements.txt` file is modified (in\naddition to watching the `pyproject.toml`).\n\nGlobs are supported, following the syntax of the [`glob`](https://docs.rs/glob/0.3.1/glob/struct.Pattern.html)\ncrate. For example, to invalidate the cache whenever a `.toml` file in the project directory\nor any of its subdirectories is modified, you can specify `cache-keys = [{ file = \"**/*.toml\" }]`.\nNote that the use of globs can be expensive, as uv may need to walk the filesystem to\ndetermine whether any files have changed.\n\nCache keys can also include version control information. For example, if a project uses\n`setuptools_scm` to read its version from a Git commit, you can specify `cache-keys = [{ git = { commit = true }, { file = \"pyproject.toml\" }]`\nto include the current Git commit hash in the cache key (in addition to the\n`pyproject.toml`). Git tags are also supported via `cache-keys = [{ git = { commit = true, tags = true } }]`.\n\nCache keys can also include environment variables. For example, if a project relies on\n`MACOSX_DEPLOYMENT_TARGET` or other environment variables to determine its behavior, you can\nspecify `cache-keys = [{ env = \"MACOSX_DEPLOYMENT_TARGET\" }]` to invalidate the cache\nwhenever the environment variable changes.\n\nCache keys only affect the project defined by the `pyproject.toml` in which they're\nspecified (as opposed to, e.g., affecting all members in a workspace), and all paths and\nglobs are interpreted as relative to the project directory.", "type": [ "array", "null" @@ -64,7 +64,7 @@ } }, "check-url": { - "description": "Check an index URL for existing files to skip duplicate uploads.\n\nThis option allows retrying publishing that failed after only some, but not all files have been uploaded, and handles error due to parallel uploads of the same file.\n\nBefore uploading, the index is checked. If the exact same file already exists in the index, the file will not be uploaded. If an error occurred during the upload, the index is checked again, to handle cases where the identical file was uploaded twice in parallel.\n\nThe exact behavior will vary based on the index. When uploading to PyPI, uploading the same file succeeds even without `--check-url`, while most other indexes error.\n\nThe index must provide one of the supported hashes (SHA-256, SHA-384, or SHA-512).", + "description": "Check an index URL for existing files to skip duplicate uploads.\n\nThis option allows retrying publishing that failed after only some, but not all files have\nbeen uploaded, and handles error due to parallel uploads of the same file.\n\nBefore uploading, the index is checked. If the exact same file already exists in the index,\nthe file will not be uploaded. If an error occurred during the upload, the index is checked\nagain, to handle cases where the identical file was uploaded twice in parallel.\n\nThe exact behavior will vary based on the index. When uploading to PyPI, uploading the same\nfile succeeds even without `--check-url`, while most other indexes error.\n\nThe index must provide one of the supported hashes (SHA-256, SHA-384, or SHA-512).", "anyOf": [ { "$ref": "#/definitions/IndexUrl" @@ -75,29 +75,29 @@ ] }, "compile-bytecode": { - "description": "Compile Python files to bytecode after installation.\n\nBy default, uv does not compile Python (`.py`) files to bytecode (`__pycache__/*.pyc`); instead, compilation is performed lazily the first time a module is imported. For use-cases in which start time is critical, such as CLI applications and Docker containers, this option can be enabled to trade longer installation times for faster start times.\n\nWhen enabled, uv will process the entire site-packages directory (including packages that are not being modified by the current operation) for consistency. Like pip, it will also ignore errors.", + "description": "Compile Python files to bytecode after installation.\n\nBy default, uv does not compile Python (`.py`) files to bytecode (`__pycache__/*.pyc`);\ninstead, compilation is performed lazily the first time a module is imported. For use-cases\nin which start time is critical, such as CLI applications and Docker containers, this option\ncan be enabled to trade longer installation times for faster start times.\n\nWhen enabled, uv will process the entire site-packages directory (including packages that\nare not being modified by the current operation) for consistency. Like pip, it will also\nignore errors.", "type": [ "boolean", "null" ] }, "concurrent-builds": { - "description": "The maximum number of source distributions that uv will build concurrently at any given time.\n\nDefaults to the number of available CPU cores.", + "description": "The maximum number of source distributions that uv will build concurrently at any given\ntime.\n\nDefaults to the number of available CPU cores.", "type": [ "integer", "null" ], "format": "uint", - "minimum": 1.0 + "minimum": 1 }, "concurrent-downloads": { - "description": "The maximum number of in-flight concurrent downloads that uv will perform at any given time.", + "description": "The maximum number of in-flight concurrent downloads that uv will perform at any given\ntime.", "type": [ "integer", "null" ], "format": "uint", - "minimum": 1.0 + "minimum": 1 }, "concurrent-installs": { "description": "The number of threads used when installing and unzipping packages.\n\nDefaults to the number of available CPU cores.", @@ -106,10 +106,10 @@ "null" ], "format": "uint", - "minimum": 1.0 + "minimum": 1 }, "config-settings": { - "description": "Settings to pass to the [PEP 517](https://peps.python.org/pep-0517/) build backend, specified as `KEY=VALUE` pairs.", + "description": "Settings to pass to the [PEP 517](https://peps.python.org/pep-0517/) build backend,\nspecified as `KEY=VALUE` pairs.", "anyOf": [ { "$ref": "#/definitions/ConfigSettings" @@ -152,7 +152,7 @@ ] }, "dependency-groups": { - "description": "Additional settings for `dependency-groups`.\n\nCurrently this can only be used to add `requires-python` constraints to dependency groups (typically to inform uv that your dev tooling has a higher python requirement than your actual project).\n\nThis cannot be used to define dependency groups, use the top-level `[dependency-groups]` table for that.", + "description": "Additional settings for `dependency-groups`.\n\nCurrently this can only be used to add `requires-python` constraints\nto dependency groups (typically to inform uv that your dev tooling\nhas a higher python requirement than your actual project).\n\nThis cannot be used to define dependency groups, use the top-level\n`[dependency-groups]` table for that.", "anyOf": [ { "$ref": "#/definitions/ToolUvDependencyGroups" @@ -163,7 +163,7 @@ ] }, "dependency-metadata": { - "description": "Pre-defined static metadata for dependencies of the project (direct or transitive). When provided, enables the resolver to use the specified metadata instead of querying the registry or building the relevant package from source.\n\nMetadata should be provided in adherence with the [Metadata 2.3](https://packaging.python.org/en/latest/specifications/core-metadata/) standard, though only the following fields are respected:\n\n- `name`: The name of the package. - (Optional) `version`: The version of the package. If omitted, the metadata will be applied to all versions of the package. - (Optional) `requires-dist`: The dependencies of the package (e.g., `werkzeug>=0.14`). - (Optional) `requires-python`: The Python version required by the package (e.g., `>=3.10`). - (Optional) `provides-extras`: The extras provided by the package.", + "description": "Pre-defined static metadata for dependencies of the project (direct or transitive). When\nprovided, enables the resolver to use the specified metadata instead of querying the\nregistry or building the relevant package from source.\n\nMetadata should be provided in adherence with the [Metadata 2.3](https://packaging.python.org/en/latest/specifications/core-metadata/)\nstandard, though only the following fields are respected:\n\n- `name`: The name of the package.\n- (Optional) `version`: The version of the package. If omitted, the metadata will be applied\n to all versions of the package.\n- (Optional) `requires-dist`: The dependencies of the package (e.g., `werkzeug>=0.14`).\n- (Optional) `requires-python`: The Python version required by the package (e.g., `>=3.10`).\n- (Optional) `provides-extras`: The extras provided by the package.", "type": [ "array", "null" @@ -193,7 +193,7 @@ } }, "exclude-newer": { - "description": "Limit candidate packages to those that were uploaded prior to a given point in time.\n\nAccepts a superset of [RFC 3339](https://www.rfc-editor.org/rfc/rfc3339.html) (e.g., `2006-12-02T02:07:43Z`). A full timestamp is required to ensure that the resolver will behave consistently across timezones.", + "description": "Limit candidate packages to those that were uploaded prior to a given point in time.\n\nAccepts a superset of [RFC 3339](https://www.rfc-editor.org/rfc/rfc3339.html) (e.g.,\n`2006-12-02T02:07:43Z`). A full timestamp is required to ensure that the resolver will\nbehave consistently across timezones.", "anyOf": [ { "$ref": "#/definitions/ExcludeNewer" @@ -204,7 +204,7 @@ ] }, "extra-index-url": { - "description": "Extra URLs of package indexes to use, in addition to `--index-url`.\n\nAccepts either a repository compliant with [PEP 503](https://peps.python.org/pep-0503/) (the simple repository API), or a local directory laid out in the same format.\n\nAll indexes provided via this flag take priority over the index specified by [`index_url`](#index-url) or [`index`](#index) with `default = true`. When multiple indexes are provided, earlier values take priority.\n\nTo control uv's resolution strategy when multiple indexes are present, see [`index_strategy`](#index-strategy).\n\n(Deprecated: use `index` instead.)", + "description": "Extra URLs of package indexes to use, in addition to `--index-url`.\n\nAccepts either a repository compliant with [PEP 503](https://peps.python.org/pep-0503/)\n(the simple repository API), or a local directory laid out in the same format.\n\nAll indexes provided via this flag take priority over the index specified by\n[`index_url`](#index-url) or [`index`](#index) with `default = true`. When multiple indexes\nare provided, earlier values take priority.\n\nTo control uv's resolution strategy when multiple indexes are present, see\n[`index_strategy`](#index-strategy).\n\n(Deprecated: use `index` instead.)", "type": [ "array", "null" @@ -214,7 +214,7 @@ } }, "find-links": { - "description": "Locations to search for candidate distributions, in addition to those found in the registry indexes.\n\nIf a path, the target must be a directory that contains packages as wheel files (`.whl`) or source distributions (e.g., `.tar.gz` or `.zip`) at the top level.\n\nIf a URL, the page must contain a flat list of links to package files adhering to the formats described above.", + "description": "Locations to search for candidate distributions, in addition to those found in the registry\nindexes.\n\nIf a path, the target must be a directory that contains packages as wheel files (`.whl`) or\nsource distributions (e.g., `.tar.gz` or `.zip`) at the top level.\n\nIf a URL, the page must contain a flat list of links to package files adhering to the\nformats described above.", "type": [ "array", "null" @@ -224,7 +224,7 @@ } }, "fork-strategy": { - "description": "The strategy to use when selecting multiple versions of a given package across Python versions and platforms.\n\nBy default, uv will optimize for selecting the latest version of each package for each supported Python version (`requires-python`), while minimizing the number of selected versions across platforms.\n\nUnder `fewest`, uv will minimize the number of selected versions for each package, preferring older versions that are compatible with a wider range of supported Python versions or platforms.", + "description": "The strategy to use when selecting multiple versions of a given package across Python\nversions and platforms.\n\nBy default, uv will optimize for selecting the latest version of each package for each\nsupported Python version (`requires-python`), while minimizing the number of selected\nversions across platforms.\n\nUnder `fewest`, uv will minimize the number of selected versions for each package,\npreferring older versions that are compatible with a wider range of supported Python\nversions or platforms.", "anyOf": [ { "$ref": "#/definitions/ForkStrategy" @@ -235,18 +235,18 @@ ] }, "index": { - "description": "The indexes to use when resolving dependencies.\n\nAccepts either a repository compliant with [PEP 503](https://peps.python.org/pep-0503/) (the simple repository API), or a local directory laid out in the same format.\n\nIndexes are considered in the order in which they're defined, such that the first-defined index has the highest priority. Further, the indexes provided by this setting are given higher priority than any indexes specified via [`index_url`](#index-url) or [`extra_index_url`](#extra-index-url). uv will only consider the first index that contains a given package, unless an alternative [index strategy](#index-strategy) is specified.\n\nIf an index is marked as `explicit = true`, it will be used exclusively for the dependencies that select it explicitly via `[tool.uv.sources]`, as in:\n\n```toml [[tool.uv.index]] name = \"pytorch\" url = \"https://download.pytorch.org/whl/cu121\" explicit = true\n\n[tool.uv.sources] torch = { index = \"pytorch\" } ```\n\nIf an index is marked as `default = true`, it will be moved to the end of the prioritized list, such that it is given the lowest priority when resolving packages. Additionally, marking an index as default will disable the PyPI default index.", - "default": null, + "description": "The indexes to use when resolving dependencies.\n\nAccepts either a repository compliant with [PEP 503](https://peps.python.org/pep-0503/)\n(the simple repository API), or a local directory laid out in the same format.\n\nIndexes are considered in the order in which they're defined, such that the first-defined\nindex has the highest priority. Further, the indexes provided by this setting are given\nhigher priority than any indexes specified via [`index_url`](#index-url) or\n[`extra_index_url`](#extra-index-url). uv will only consider the first index that contains\na given package, unless an alternative [index strategy](#index-strategy) is specified.\n\nIf an index is marked as `explicit = true`, it will be used exclusively for the\ndependencies that select it explicitly via `[tool.uv.sources]`, as in:\n\n```toml\n[[tool.uv.index]]\nname = \"pytorch\"\nurl = \"https://download.pytorch.org/whl/cu121\"\nexplicit = true\n\n[tool.uv.sources]\ntorch = { index = \"pytorch\" }\n```\n\nIf an index is marked as `default = true`, it will be moved to the end of the prioritized list, such that it is\ngiven the lowest priority when resolving packages. Additionally, marking an index as default will disable the\nPyPI default index.", "type": [ "array", "null" ], + "default": null, "items": { "$ref": "#/definitions/Index" } }, "index-strategy": { - "description": "The strategy to use when resolving against multiple index URLs.\n\nBy default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (`first-index`). This prevents \"dependency confusion\" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.", + "description": "The strategy to use when resolving against multiple index URLs.\n\nBy default, uv will stop at the first index on which a given package is available, and\nlimit resolutions to those present on that first index (`first-index`). This prevents\n\"dependency confusion\" attacks, whereby an attacker can upload a malicious package under the\nsame name to an alternate index.", "anyOf": [ { "$ref": "#/definitions/IndexStrategy" @@ -257,7 +257,7 @@ ] }, "index-url": { - "description": "The URL of the Python package index (by default: ).\n\nAccepts either a repository compliant with [PEP 503](https://peps.python.org/pep-0503/) (the simple repository API), or a local directory laid out in the same format.\n\nThe index provided by this setting is given lower priority than any indexes specified via [`extra_index_url`](#extra-index-url) or [`index`](#index).\n\n(Deprecated: use `index` instead.)", + "description": "The URL of the Python package index (by default: ).\n\nAccepts either a repository compliant with [PEP 503](https://peps.python.org/pep-0503/)\n(the simple repository API), or a local directory laid out in the same format.\n\nThe index provided by this setting is given lower priority than any indexes specified via\n[`extra_index_url`](#extra-index-url) or [`index`](#index).\n\n(Deprecated: use `index` instead.)", "anyOf": [ { "$ref": "#/definitions/IndexUrl" @@ -268,7 +268,7 @@ ] }, "keyring-provider": { - "description": "Attempt to use `keyring` for authentication for index URLs.\n\nAt present, only `--keyring-provider subprocess` is supported, which configures uv to use the `keyring` CLI to handle authentication.", + "description": "Attempt to use `keyring` for authentication for index URLs.\n\nAt present, only `--keyring-provider subprocess` is supported, which configures uv to\nuse the `keyring` CLI to handle authentication.", "anyOf": [ { "$ref": "#/definitions/KeyringProviderType" @@ -279,7 +279,7 @@ ] }, "link-mode": { - "description": "The method to use when installing packages from the global cache.\n\nDefaults to `clone` (also known as Copy-on-Write) on macOS, and `hardlink` on Linux and Windows.", + "description": "The method to use when installing packages from the global cache.\n\nDefaults to `clone` (also known as Copy-on-Write) on macOS, and `hardlink` on Linux and\nWindows.", "anyOf": [ { "$ref": "#/definitions/LinkMode" @@ -290,21 +290,21 @@ ] }, "managed": { - "description": "Whether the project is managed by uv. If `false`, uv will ignore the project when `uv run` is invoked.", + "description": "Whether the project is managed by uv. If `false`, uv will ignore the project when\n`uv run` is invoked.", "type": [ "boolean", "null" ] }, "native-tls": { - "description": "Whether to load TLS certificates from the platform's native certificate store.\n\nBy default, uv loads certificates from the bundled `webpki-roots` crate. The `webpki-roots` are a reliable set of trust roots from Mozilla, and including them in uv improves portability and performance (especially on macOS).\n\nHowever, in some cases, you may want to use the platform's native certificate store, especially if you're relying on a corporate trust root (e.g., for a mandatory proxy) that's included in your system's certificate store.", + "description": "Whether to load TLS certificates from the platform's native certificate store.\n\nBy default, uv loads certificates from the bundled `webpki-roots` crate. The\n`webpki-roots` are a reliable set of trust roots from Mozilla, and including them in uv\nimproves portability and performance (especially on macOS).\n\nHowever, in some cases, you may want to use the platform's native certificate store,\nespecially if you're relying on a corporate trust root (e.g., for a mandatory proxy) that's\nincluded in your system's certificate store.", "type": [ "boolean", "null" ] }, "no-binary": { - "description": "Don't install pre-built wheels.\n\nThe given packages will be built and installed from source. The resolver will still use pre-built wheels to extract package metadata, if available.", + "description": "Don't install pre-built wheels.\n\nThe given packages will be built and installed from source. The resolver will still use\npre-built wheels to extract package metadata, if available.", "type": [ "boolean", "null" @@ -321,21 +321,21 @@ } }, "no-build": { - "description": "Don't build source distributions.\n\nWhen enabled, resolving will not run arbitrary Python code. The cached wheels of already-built source distributions will be reused, but operations that require building distributions will exit with an error.", + "description": "Don't build source distributions.\n\nWhen enabled, resolving will not run arbitrary Python code. The cached wheels of\nalready-built source distributions will be reused, but operations that require building\ndistributions will exit with an error.", "type": [ "boolean", "null" ] }, "no-build-isolation": { - "description": "Disable isolation when building source distributions.\n\nAssumes that build dependencies specified by [PEP 518](https://peps.python.org/pep-0518/) are already installed.", + "description": "Disable isolation when building source distributions.\n\nAssumes that build dependencies specified by [PEP 518](https://peps.python.org/pep-0518/)\nare already installed.", "type": [ "boolean", "null" ] }, "no-build-isolation-package": { - "description": "Disable isolation when building source distributions for a specific package.\n\nAssumes that the packages' build dependencies specified by [PEP 518](https://peps.python.org/pep-0518/) are already installed.", + "description": "Disable isolation when building source distributions for a specific package.\n\nAssumes that the packages' build dependencies specified by [PEP 518](https://peps.python.org/pep-0518/)\nare already installed.", "type": [ "array", "null" @@ -355,21 +355,21 @@ } }, "no-cache": { - "description": "Avoid reading from or writing to the cache, instead using a temporary directory for the duration of the operation.", + "description": "Avoid reading from or writing to the cache, instead using a temporary directory for the\nduration of the operation.", "type": [ "boolean", "null" ] }, "no-index": { - "description": "Ignore all registry indexes (e.g., PyPI), instead relying on direct URL dependencies and those provided via `--find-links`.", + "description": "Ignore all registry indexes (e.g., PyPI), instead relying on direct URL dependencies and\nthose provided via `--find-links`.", "type": [ "boolean", "null" ] }, "no-sources": { - "description": "Ignore the `tool.uv.sources` table when resolving dependencies. Used to lock against the standards-compliant, publishable package metadata, as opposed to using any local or Git sources.", + "description": "Ignore the `tool.uv.sources` table when resolving dependencies. Used to lock against the\nstandards-compliant, publishable package metadata, as opposed to using any local or Git\nsources.", "type": [ "boolean", "null" @@ -393,7 +393,7 @@ } }, "package": { - "description": "Whether the project should be considered a Python package, or a non-package (\"virtual\") project.\n\nPackages are built and installed into the virtual environment in editable mode and thus require a build backend, while virtual projects are _not_ built or installed; instead, only their dependencies are included in the virtual environment.\n\nCreating a package requires that a `build-system` is present in the `pyproject.toml`, and that the project adheres to a structure that adheres to the build backend's expectations (e.g., a `src` layout).", + "description": "Whether the project should be considered a Python package, or a non-package (\"virtual\")\nproject.\n\nPackages are built and installed into the virtual environment in editable mode and thus\nrequire a build backend, while virtual projects are _not_ built or installed; instead, only\ntheir dependencies are included in the virtual environment.\n\nCreating a package requires that a `build-system` is present in the `pyproject.toml`, and\nthat the project adheres to a structure that adheres to the build backend's expectations\n(e.g., a `src` layout).", "type": [ "boolean", "null" @@ -410,7 +410,7 @@ ] }, "prerelease": { - "description": "The strategy to use when considering pre-release versions.\n\nBy default, uv will accept pre-releases for packages that _only_ publish pre-releases, along with first-party requirements that contain an explicit pre-release marker in the declared specifiers (`if-necessary-or-explicit`).", + "description": "The strategy to use when considering pre-release versions.\n\nBy default, uv will accept pre-releases for packages that _only_ publish pre-releases,\nalong with first-party requirements that contain an explicit pre-release marker in the\ndeclared specifiers (`if-necessary-or-explicit`).", "anyOf": [ { "$ref": "#/definitions/PrereleaseMode" @@ -428,15 +428,18 @@ ] }, "publish-url": { - "description": "The URL for publishing packages to the Python package index (by default: ).", - "type": [ - "string", - "null" - ], - "format": "uri" + "description": "The URL for publishing packages to the Python package index (by default:\n).", + "anyOf": [ + { + "$ref": "#/definitions/DisplaySafeUrl" + }, + { + "type": "null" + } + ] }, "pypy-install-mirror": { - "description": "Mirror URL to use for downloading managed PyPy installations.\n\nBy default, managed PyPy installations are downloaded from [downloads.python.org](https://downloads.python.org/). This variable can be set to a mirror URL to use a different source for PyPy installations. The provided URL will replace `https://downloads.python.org/pypy` in, e.g., `https://downloads.python.org/pypy/pypy3.8-v7.3.7-osx64.tar.bz2`.\n\nDistributions can be read from a local directory by using the `file://` URL scheme.", + "description": "Mirror URL to use for downloading managed PyPy installations.\n\nBy default, managed PyPy installations are downloaded from [downloads.python.org](https://downloads.python.org/).\nThis variable can be set to a mirror URL to use a different source for PyPy installations.\nThe provided URL will replace `https://downloads.python.org/pypy` in, e.g., `https://downloads.python.org/pypy/pypy3.8-v7.3.7-osx64.tar.bz2`.\n\nDistributions can be read from a\nlocal directory by using the `file://` URL scheme.", "type": [ "string", "null" @@ -461,14 +464,14 @@ ] }, "python-install-mirror": { - "description": "Mirror URL for downloading managed Python installations.\n\nBy default, managed Python installations are downloaded from [`python-build-standalone`](https://github.com/astral-sh/python-build-standalone). This variable can be set to a mirror URL to use a different source for Python installations. The provided URL will replace `https://github.com/astral-sh/python-build-standalone/releases/download` in, e.g., `https://github.com/astral-sh/python-build-standalone/releases/download/20240713/cpython-3.12.4%2B20240713-aarch64-apple-darwin-install_only.tar.gz`.\n\nDistributions can be read from a local directory by using the `file://` URL scheme.", + "description": "Mirror URL for downloading managed Python installations.\n\nBy default, managed Python installations are downloaded from [`python-build-standalone`](https://github.com/astral-sh/python-build-standalone).\nThis variable can be set to a mirror URL to use a different source for Python installations.\nThe provided URL will replace `https://github.com/astral-sh/python-build-standalone/releases/download` in, e.g., `https://github.com/astral-sh/python-build-standalone/releases/download/20240713/cpython-3.12.4%2B20240713-aarch64-apple-darwin-install_only.tar.gz`.\n\nDistributions can be read from a local directory by using the `file://` URL scheme.", "type": [ "string", "null" ] }, "python-preference": { - "description": "Whether to prefer using Python installations that are already present on the system, or those that are downloaded and installed by uv.", + "description": "Whether to prefer using Python installations that are already present on the system, or\nthose that are downloaded and installed by uv.", "anyOf": [ { "$ref": "#/definitions/PythonPreference" @@ -486,7 +489,7 @@ ] }, "reinstall-package": { - "description": "Reinstall a specific package, regardless of whether it's already installed. Implies `refresh-package`.", + "description": "Reinstall a specific package, regardless of whether it's already installed. Implies\n`refresh-package`.", "type": [ "array", "null" @@ -506,7 +509,7 @@ } }, "required-version": { - "description": "Enforce a requirement on the version of uv.\n\nIf the version of uv does not meet the requirement at runtime, uv will exit with an error.\n\nAccepts a [PEP 440](https://peps.python.org/pep-0440/) specifier, like `==0.5.0` or `>=0.5.0`.", + "description": "Enforce a requirement on the version of uv.\n\nIf the version of uv does not meet the requirement at runtime, uv will exit\nwith an error.\n\nAccepts a [PEP 440](https://peps.python.org/pep-0440/) specifier, like `==0.5.0` or `>=0.5.0`.", "anyOf": [ { "$ref": "#/definitions/RequiredVersion" @@ -517,7 +520,7 @@ ] }, "resolution": { - "description": "The strategy to use when selecting between the different compatible versions for a given package requirement.\n\nBy default, uv will use the latest compatible version of each package (`highest`).", + "description": "The strategy to use when selecting between the different compatible versions for a given\npackage requirement.\n\nBy default, uv will use the latest compatible version of each package (`highest`).", "anyOf": [ { "$ref": "#/definitions/ResolutionMode" @@ -528,7 +531,7 @@ ] }, "sources": { - "description": "The sources to use when resolving dependencies.\n\n`tool.uv.sources` enriches the dependency metadata with additional sources, incorporated during development. A dependency source can be a Git repository, a URL, a local path, or an alternative registry.\n\nSee [Dependencies](https://docs.astral.sh/uv/concepts/projects/dependencies/) for more.", + "description": "The sources to use when resolving dependencies.\n\n`tool.uv.sources` enriches the dependency metadata with additional sources, incorporated\nduring development. A dependency source can be a Git repository, a URL, a local path, or an\nalternative registry.\n\nSee [Dependencies](https://docs.astral.sh/uv/concepts/projects/dependencies/) for more.", "anyOf": [ { "$ref": "#/definitions/ToolUvSources" @@ -539,7 +542,7 @@ ] }, "trusted-publishing": { - "description": "Configure trusted publishing via GitHub Actions.\n\nBy default, uv checks for trusted publishing when running in GitHub Actions, but ignores it if it isn't configured or the workflow doesn't have enough permissions (e.g., a pull request from a fork).", + "description": "Configure trusted publishing via GitHub Actions.\n\nBy default, uv checks for trusted publishing when running in GitHub Actions, but ignores it\nif it isn't configured or the workflow doesn't have enough permissions (e.g., a pull request\nfrom a fork).", "anyOf": [ { "$ref": "#/definitions/TrustedPublishing" @@ -557,7 +560,7 @@ ] }, "upgrade-package": { - "description": "Allow upgrades for a specific package, ignoring pinned versions in any existing output file.\n\nAccepts both standalone package names (`ruff`) and version specifiers (`ruff<0.5.0`).", + "description": "Allow upgrades for a specific package, ignoring pinned versions in any existing output\nfile.\n\nAccepts both standalone package names (`ruff`) and version specifiers (`ruff<0.5.0`).", "type": [ "array", "null" @@ -578,6 +581,7 @@ ] } }, + "additionalProperties": false, "definitions": { "AddBoundsKind": { "description": "The default version specifier when adding a dependency.", @@ -585,49 +589,37 @@ { "description": "Only a lower bound, e.g., `>=1.2.3`.", "type": "string", - "enum": [ - "lower" - ] + "const": "lower" }, { "description": "Allow the same major version, similar to the semver caret, e.g., `>=1.2.3, <2.0.0`.\n\nLeading zeroes are skipped, e.g. `>=0.1.2, <0.2.0`.", "type": "string", - "enum": [ - "major" - ] + "const": "major" }, { "description": "Allow the same minor version, similar to the semver tilde, e.g., `>=1.2.3, <1.3.0`.\n\nLeading zeroes are skipped, e.g. `>=0.1.2, <0.1.3`.", "type": "string", - "enum": [ - "minor" - ] + "const": "minor" }, { "description": "Pin the exact version, e.g., `==1.2.3`.\n\nThis option is not recommended, as versions are already pinned in the uv lockfile.", "type": "string", - "enum": [ - "exact" - ] + "const": "exact" } ] }, "AnnotationStyle": { - "description": "Indicate the style of annotation comments, used to indicate the dependencies that requested each package.", + "description": "Indicate the style of annotation comments, used to indicate the dependencies that requested each\npackage.", "oneOf": [ { "description": "Render the annotations on a single, comma-separated line.", "type": "string", - "enum": [ - "line" - ] + "const": "line" }, { "description": "Render each annotation on its own line.", "type": "string", - "enum": [ - "split" - ] + "const": "split" } ] }, @@ -635,90 +627,84 @@ "description": "When to use authentication.", "oneOf": [ { - "description": "Authenticate when necessary.\n\nIf credentials are provided, they will be used. Otherwise, an unauthenticated request will be attempted first. If the request fails, uv will search for credentials. If credentials are found, an authenticated request will be attempted.", + "description": "Authenticate when necessary.\n\nIf credentials are provided, they will be used. Otherwise, an unauthenticated request will\nbe attempted first. If the request fails, uv will search for credentials. If credentials are\nfound, an authenticated request will be attempted.", "type": "string", - "enum": [ - "auto" - ] + "const": "auto" }, { - "description": "Always authenticate.\n\nIf credentials are not provided, uv will eagerly search for credentials. If credentials cannot be found, uv will error instead of attempting an unauthenticated request.", + "description": "Always authenticate.\n\nIf credentials are not provided, uv will eagerly search for credentials. If credentials\ncannot be found, uv will error instead of attempting an unauthenticated request.", "type": "string", - "enum": [ - "always" - ] + "const": "always" }, { "description": "Never authenticate.\n\nIf credentials are provided, uv will error. uv will not search for credentials.", "type": "string", - "enum": [ - "never" - ] + "const": "never" } ] }, "BuildBackendSettings": { - "description": "Settings for the uv build backend (`uv_build`).\n\n!!! note\n\nThe uv build backend is currently in preview and may change in any future release.\n\nNote that those settings only apply when using the `uv_build` backend, other build backends (such as hatchling) have their own configuration.\n\nAll options that accept globs use the portable glob patterns from [PEP 639](https://packaging.python.org/en/latest/specifications/glob-patterns/).", + "description": "Settings for the uv build backend (`uv_build`).\n\n!!! note\n\n The uv build backend is currently in preview and may change in any future release.\n\nNote that those settings only apply when using the `uv_build` backend, other build backends\n(such as hatchling) have their own configuration.\n\nAll options that accept globs use the portable glob patterns from\n[PEP 639](https://packaging.python.org/en/latest/specifications/glob-patterns/).", "type": "object", "properties": { "data": { - "description": "Data includes for wheels.\n\nEach entry is a directory, whose contents are copied to the matching directory in the wheel in `-.data/(purelib|platlib|headers|scripts|data)`. Upon installation, this data is moved to its target location, as defined by . Usually, small data files are included by placing them in the Python module instead of using data includes.\n\n- `scripts`: Installed to the directory for executables, `/bin` on Unix or `\\Scripts` on Windows. This directory is added to `PATH` when the virtual environment is activated or when using `uv run`, so this data type can be used to install additional binaries. Consider using `project.scripts` instead for Python entrypoints. - `data`: Installed over the virtualenv environment root.\n\nWarning: This may override existing files!\n\n- `headers`: Installed to the include directory. Compilers building Python packages with this package as build requirement use the include directory to find additional header files. - `purelib` and `platlib`: Installed to the `site-packages` directory. It is not recommended to uses these two options.", + "description": "Data includes for wheels.\n\nEach entry is a directory, whose contents are copied to the matching directory in the wheel\nin `-.data/(purelib|platlib|headers|scripts|data)`. Upon installation, this\ndata is moved to its target location, as defined by\n. Usually, small\ndata files are included by placing them in the Python module instead of using data includes.\n\n- `scripts`: Installed to the directory for executables, `/bin` on Unix or\n `\\Scripts` on Windows. This directory is added to `PATH` when the virtual\n environment is activated or when using `uv run`, so this data type can be used to install\n additional binaries. Consider using `project.scripts` instead for Python entrypoints.\n- `data`: Installed over the virtualenv environment root.\n\n Warning: This may override existing files!\n\n- `headers`: Installed to the include directory. Compilers building Python packages\n with this package as build requirement use the include directory to find additional header\n files.\n- `purelib` and `platlib`: Installed to the `site-packages` directory. It is not recommended\n to uses these two options.", + "allOf": [ + { + "$ref": "#/definitions/WheelDataIncludes" + } + ], "default": { "data": null, "headers": null, "platlib": null, "purelib": null, "scripts": null - }, - "allOf": [ - { - "$ref": "#/definitions/WheelDataIncludes" - } - ] + } }, "default-excludes": { "description": "If set to `false`, the default excludes aren't applied.\n\nDefault excludes: `__pycache__`, `*.pyc`, and `*.pyo`.", - "default": true, - "type": "boolean" + "type": "boolean", + "default": true }, "module-name": { - "description": "The name of the module directory inside `module-root`.\n\nThe default module name is the package name with dots and dashes replaced by underscores.\n\nPackage names need to be valid Python identifiers, and the directory needs to contain a `__init__.py`. An exception are stubs packages, whose name ends with `-stubs`, with the stem being the module name, and which contain a `__init__.pyi` file.\n\nFor namespace packages with a single module, the path can be dotted, e.g., `foo.bar` or `foo-stubs.bar`.\n\nNote that using this option runs the risk of creating two packages with different names but the same module names. Installing such packages together leads to unspecified behavior, often with corrupted files or directory trees.", - "default": null, + "description": "The name of the module directory inside `module-root`.\n\nThe default module name is the package name with dots and dashes replaced by underscores.\n\nPackage names need to be valid Python identifiers, and the directory needs to contain a\n`__init__.py`. An exception are stubs packages, whose name ends with `-stubs`, with the stem\nbeing the module name, and which contain a `__init__.pyi` file.\n\nFor namespace packages with a single module, the path can be dotted, e.g., `foo.bar` or\n`foo-stubs.bar`.\n\nNote that using this option runs the risk of creating two packages with different names but\nthe same module names. Installing such packages together leads to unspecified behavior,\noften with corrupted files or directory trees.", "type": [ "string", "null" - ] + ], + "default": null }, "module-root": { "description": "The directory that contains the module directory.\n\nCommon values are `src` (src layout, the default) or an empty path (flat layout).", - "default": "src", - "type": "string" + "type": "string", + "default": "src" }, "namespace": { - "description": "Build a namespace package.\n\nBuild a PEP 420 implicit namespace package, allowing more than one root `__init__.py`.\n\nUse this option when the namespace package contains multiple root `__init__.py`, for namespace packages with a single root `__init__.py` use a dotted `module-name` instead.\n\nTo compare dotted `module-name` and `namespace = true`, the first example below can be expressed with `module-name = \"cloud.database\"`: There is one root `__init__.py` `database`. In the second example, we have three roots (`cloud.database`, `cloud.database_pro`, `billing.modules.database_pro`), so `namespace = true` is required.\n\n```text src └── cloud └── database ├── __init__.py ├── query_builder │ └── __init__.py └── sql ├── parser.py └── __init__.py ```\n\n```text src ├── cloud │ ├── database │ │ ├── __init__.py │ │ ├── query_builder │ │ │ └── __init__.py │ │ └── sql │ │ ├── __init__.py │ │ └── parser.py │ └── database_pro │ ├── __init__.py │ └── query_builder.py └── billing └── modules └── database_pro ├── __init__.py └── sql.py ```", - "default": false, - "type": "boolean" + "description": "Build a namespace package.\n\nBuild a PEP 420 implicit namespace package, allowing more than one root `__init__.py`.\n\nUse this option when the namespace package contains multiple root `__init__.py`, for\nnamespace packages with a single root `__init__.py` use a dotted `module-name` instead.\n\nTo compare dotted `module-name` and `namespace = true`, the first example below can be\nexpressed with `module-name = \"cloud.database\"`: There is one root `__init__.py` `database`.\nIn the second example, we have three roots (`cloud.database`, `cloud.database_pro`,\n`billing.modules.database_pro`), so `namespace = true` is required.\n\n```text\nsrc\n└── cloud\n └── database\n ├── __init__.py\n ├── query_builder\n │ └── __init__.py\n └── sql\n ├── parser.py\n └── __init__.py\n```\n\n```text\nsrc\n├── cloud\n│ ├── database\n│ │ ├── __init__.py\n│ │ ├── query_builder\n│ │ │ └── __init__.py\n│ │ └── sql\n│ │ ├── __init__.py\n│ │ └── parser.py\n│ └── database_pro\n│ ├── __init__.py\n│ └── query_builder.py\n└── billing\n └── modules\n └── database_pro\n ├── __init__.py\n └── sql.py\n```", + "type": "boolean", + "default": false }, "source-exclude": { "description": "Glob expressions which files and directories to exclude from the source distribution.", - "default": [], "type": "array", + "default": [], "items": { "type": "string" } }, "source-include": { - "description": "Glob expressions which files and directories to additionally include in the source distribution.\n\n`pyproject.toml` and the contents of the module directory are always included.", - "default": [], + "description": "Glob expressions which files and directories to additionally include in the source\ndistribution.\n\n`pyproject.toml` and the contents of the module directory are always included.", "type": "array", + "default": [], "items": { "type": "string" } }, "wheel-exclude": { "description": "Glob expressions which files and directories to exclude from the wheel.", - "default": [], "type": "array", + "default": [], "items": { "type": "string" } @@ -734,54 +720,54 @@ { "description": "Ex) `{ file = \"Cargo.lock\" }` or `{ file = \"**/*.toml\" }`", "type": "object", - "required": [ - "file" - ], "properties": { "file": { "type": "string" } }, - "additionalProperties": false + "additionalProperties": false, + "required": [ + "file" + ] }, { "description": "Ex) `{ dir = \"src\" }`", "type": "object", - "required": [ - "dir" - ], "properties": { "dir": { "type": "string" } }, - "additionalProperties": false + "additionalProperties": false, + "required": [ + "dir" + ] }, { "description": "Ex) `{ git = true }` or `{ git = { commit = true, tags = false } }`", "type": "object", - "required": [ - "git" - ], "properties": { "git": { "$ref": "#/definitions/GitPattern" } }, - "additionalProperties": false + "additionalProperties": false, + "required": [ + "git" + ] }, { "description": "Ex) `{ env = \"UV_CACHE_INFO\" }`", "type": "object", - "required": [ - "env" - ], "properties": { "env": { "type": "string" } }, - "additionalProperties": false + "additionalProperties": false, + "required": [ + "env" + ] } ] }, @@ -801,7 +787,7 @@ ] }, "ConfigSettings": { - "description": "Settings to pass to a PEP 517 build backend, structured as a map from (string) key to string or list of strings.\n\nSee: ", + "description": "Settings to pass to a PEP 517 build backend, structured as a map from (string) key to string or\nlist of strings.\n\nSee: ", "type": "object", "additionalProperties": { "$ref": "#/definitions/ConfigSettingValue" @@ -813,16 +799,11 @@ { "description": "All groups are defaulted", "type": "string", - "enum": [ - "All" - ] + "const": "All" }, { "description": "A list of groups", "type": "object", - "required": [ - "List" - ], "properties": { "List": { "type": "array", @@ -831,7 +812,10 @@ } } }, - "additionalProperties": false + "additionalProperties": false, + "required": [ + "List" + ] } ] }, @@ -847,30 +831,31 @@ } } }, + "DisplaySafeUrl": { + "description": "A [`Url`] wrapper that redacts credentials when displaying the URL.\n\n`DisplaySafeUrl` wraps the standard [`url::Url`] type, providing functionality to mask\nsecrets by default when the URL is displayed or logged. This helps prevent accidental\nexposure of sensitive information in logs and debug output.\n\n# Examples\n\n```\nuse uv_redacted::DisplaySafeUrl;\nuse std::str::FromStr;\n\n// Create a `DisplaySafeUrl` from a `&str`\nlet mut url = DisplaySafeUrl::parse(\"https://user:password@example.com\").unwrap();\n\n// Display will mask secrets\nassert_eq!(url.to_string(), \"https://user:****@example.com/\");\n\n// You can still access the username and password\nassert_eq!(url.username(), \"user\");\nassert_eq!(url.password(), Some(\"password\"));\n\n// And you can still update the username and password\nlet _ = url.set_username(\"new_user\");\nlet _ = url.set_password(Some(\"new_password\"));\nassert_eq!(url.username(), \"new_user\");\nassert_eq!(url.password(), Some(\"new_password\"));\n\n// It is also possible to remove the credentials entirely\nurl.remove_credentials();\nassert_eq!(url.username(), \"\");\nassert_eq!(url.password(), None);\n```", + "type": "string", + "format": "uri" + }, "ExcludeNewer": { "description": "Exclude distributions uploaded after the given timestamp.\n\nAccepts both RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`) and local dates in the same format (e.g., `2006-12-02`).", "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}(T\\d{2}:\\d{2}:\\d{2}(Z|[+-]\\d{2}:\\d{2}))?$" }, "ExtraName": { - "description": "The normalized name of an extra dependency.\n\nConverts the name to lowercase and collapses runs of `-`, `_`, and `.` down to a single `-`. For example, `---`, `.`, and `__` are all converted to a single `-`.\n\nSee: - - ", + "description": "The normalized name of an extra dependency.\n\nConverts the name to lowercase and collapses runs of `-`, `_`, and `.` down to a single `-`.\nFor example, `---`, `.`, and `__` are all converted to a single `-`.\n\nSee:\n- \n- ", "type": "string" }, "ForkStrategy": { "oneOf": [ { - "description": "Optimize for selecting the fewest number of versions for each package. Older versions may be preferred if they are compatible with a wider range of supported Python versions or platforms.", + "description": "Optimize for selecting the fewest number of versions for each package. Older versions may\nbe preferred if they are compatible with a wider range of supported Python versions or\nplatforms.", "type": "string", - "enum": [ - "fewest" - ] + "const": "fewest" }, { - "description": "Optimize for selecting latest supported version of each package, for each supported Python version.", + "description": "Optimize for selecting latest supported version of each package, for each supported Python\nversion.", "type": "string", - "enum": [ - "requires-python" - ] + "const": "requires-python" } ] }, @@ -903,56 +888,53 @@ "additionalProperties": false }, "GroupName": { - "description": "The normalized name of a dependency group.\n\nSee: - - ", + "description": "The normalized name of a dependency group.\n\nSee:\n- \n- ", "type": "string" }, "Index": { "type": "object", - "required": [ - "url" - ], "properties": { - "authenticate": { - "description": "When uv should use authentication for requests to the index.\n\n```toml [[tool.uv.index]] name = \"my-index\" url = \"https:///simple\" authenticate = \"always\" ```", - "default": "auto", - "allOf": [ - { - "$ref": "#/definitions/AuthPolicy" - } - ] - }, - "default": { - "description": "Mark the index as the default index.\n\nBy default, uv uses PyPI as the default index, such that even if additional indexes are defined via `[[tool.uv.index]]`, PyPI will still be used as a fallback for packages that aren't found elsewhere. To disable the PyPI default, set `default = true` on at least one other index.\n\nMarking an index as default will move it to the front of the list of indexes, such that it is given the highest priority when resolving packages.", - "default": false, - "type": "boolean" - }, - "explicit": { - "description": "Mark the index as explicit.\n\nExplicit indexes will _only_ be used when explicitly requested via a `[tool.uv.sources]` definition, as in:\n\n```toml [[tool.uv.index]] name = \"pytorch\" url = \"https://download.pytorch.org/whl/cu121\" explicit = true\n\n[tool.uv.sources] torch = { index = \"pytorch\" } ```", - "default": false, - "type": "boolean" - }, "format": { - "description": "The format used by the index.\n\nIndexes can either be PEP 503-compliant (i.e., a PyPI-style registry implementing the Simple API) or structured as a flat list of distributions (e.g., `--find-links`). In both cases, indexes can point to either local or remote resources.", - "default": "simple", + "description": "The format used by the index.\n\nIndexes can either be PEP 503-compliant (i.e., a PyPI-style registry implementing the Simple\nAPI) or structured as a flat list of distributions (e.g., `--find-links`). In both cases,\nindexes can point to either local or remote resources.", "allOf": [ { "$ref": "#/definitions/IndexFormat" } - ] + ], + "default": "simple" + }, + "authenticate": { + "description": "When uv should use authentication for requests to the index.\n\n```toml\n[[tool.uv.index]]\nname = \"my-index\"\nurl = \"https:///simple\"\nauthenticate = \"always\"\n```", + "allOf": [ + { + "$ref": "#/definitions/AuthPolicy" + } + ], + "default": "auto" + }, + "default": { + "description": "Mark the index as the default index.\n\nBy default, uv uses PyPI as the default index, such that even if additional indexes are\ndefined via `[[tool.uv.index]]`, PyPI will still be used as a fallback for packages that\naren't found elsewhere. To disable the PyPI default, set `default = true` on at least one\nother index.\n\nMarking an index as default will move it to the front of the list of indexes, such that it\nis given the highest priority when resolving packages.", + "type": "boolean", + "default": false + }, + "explicit": { + "description": "Mark the index as explicit.\n\nExplicit indexes will _only_ be used when explicitly requested via a `[tool.uv.sources]`\ndefinition, as in:\n\n```toml\n[[tool.uv.index]]\nname = \"pytorch\"\nurl = \"https://download.pytorch.org/whl/cu121\"\nexplicit = true\n\n[tool.uv.sources]\ntorch = { index = \"pytorch\" }\n```", + "type": "boolean", + "default": false }, "ignore-error-codes": { - "description": "Status codes that uv should ignore when deciding whether to continue searching in the next index after a failure.\n\n```toml [[tool.uv.index]] name = \"my-index\" url = \"https:///simple\" ignore-error-codes = [401, 403] ```", - "default": null, + "description": "Status codes that uv should ignore when deciding whether\nto continue searching in the next index after a failure.\n\n```toml\n[[tool.uv.index]]\nname = \"my-index\"\nurl = \"https:///simple\"\nignore-error-codes = [401, 403]\n```", "type": [ "array", "null" ], + "default": null, "items": { "$ref": "#/definitions/StatusCode" } }, "name": { - "description": "The name of the index.\n\nIndex names can be used to reference indexes elsewhere in the configuration. For example, you can pin a package to a specific index by name:\n\n```toml [[tool.uv.index]] name = \"pytorch\" url = \"https://download.pytorch.org/whl/cu121\"\n\n[tool.uv.sources] torch = { index = \"pytorch\" } ```", + "description": "The name of the index.\n\nIndex names can be used to reference indexes elsewhere in the configuration. For example,\nyou can pin a package to a specific index by name:\n\n```toml\n[[tool.uv.index]]\nname = \"pytorch\"\nurl = \"https://download.pytorch.org/whl/cu121\"\n\n[tool.uv.sources]\ntorch = { index = \"pytorch\" }\n```", "anyOf": [ { "$ref": "#/definitions/IndexName" @@ -963,12 +945,15 @@ ] }, "publish-url": { - "description": "The URL of the upload endpoint.\n\nWhen using `uv publish --index `, this URL is used for publishing.\n\nA configuration for the default index PyPI would look as follows:\n\n```toml [[tool.uv.index]] name = \"pypi\" url = \"https://pypi.org/simple\" publish-url = \"https://upload.pypi.org/legacy/\" ```", - "type": [ - "string", - "null" - ], - "format": "uri" + "description": "The URL of the upload endpoint.\n\nWhen using `uv publish --index `, this URL is used for publishing.\n\nA configuration for the default index PyPI would look as follows:\n\n```toml\n[[tool.uv.index]]\nname = \"pypi\"\nurl = \"https://pypi.org/simple\"\npublish-url = \"https://upload.pypi.org/legacy/\"\n```", + "anyOf": [ + { + "$ref": "#/definitions/DisplaySafeUrl" + }, + { + "type": "null" + } + ] }, "url": { "description": "The URL of the index.\n\nExpects to receive a URL (e.g., `https://pypi.org/simple`) or a local path.", @@ -978,23 +963,22 @@ } ] } - } + }, + "required": [ + "url" + ] }, "IndexFormat": { "oneOf": [ { "description": "A PyPI-style index implementing the Simple Repository API.", "type": "string", - "enum": [ - "simple" - ] + "const": "simple" }, { "description": "A `--find-links`-style index containing a flat list of wheels and source distributions.", "type": "string", - "enum": [ - "flat" - ] + "const": "flat" } ] }, @@ -1005,25 +989,19 @@ "IndexStrategy": { "oneOf": [ { - "description": "Only use results from the first index that returns a match for a given package name.\n\nWhile this differs from pip's behavior, it's the default index strategy as it's the most secure.", + "description": "Only use results from the first index that returns a match for a given package name.\n\nWhile this differs from pip's behavior, it's the default index strategy as it's the most\nsecure.", "type": "string", - "enum": [ - "first-index" - ] + "const": "first-index" }, { - "description": "Search for every package name across all indexes, exhausting the versions from the first index before moving on to the next.\n\nIn this strategy, we look for every package across all indexes. When resolving, we attempt to use versions from the indexes in order, such that we exhaust all available versions from the first index before moving on to the next. Further, if a version is found to be incompatible in the first index, we do not reconsider that version in subsequent indexes, even if the secondary index might contain compatible versions (e.g., variants of the same versions with different ABI tags or Python version constraints).\n\nSee: ", + "description": "Search for every package name across all indexes, exhausting the versions from the first\nindex before moving on to the next.\n\nIn this strategy, we look for every package across all indexes. When resolving, we attempt\nto use versions from the indexes in order, such that we exhaust all available versions from\nthe first index before moving on to the next. Further, if a version is found to be\nincompatible in the first index, we do not reconsider that version in subsequent indexes,\neven if the secondary index might contain compatible versions (e.g., variants of the same\nversions with different ABI tags or Python version constraints).\n\nSee: ", "type": "string", - "enum": [ - "unsafe-first-match" - ] + "const": "unsafe-first-match" }, { - "description": "Search for every package name across all indexes, preferring the \"best\" version found. If a package version is in multiple indexes, only look at the entry for the first index.\n\nIn this strategy, we look for every package across all indexes. When resolving, we consider all versions from all indexes, choosing the \"best\" version found (typically, the highest compatible version).\n\nThis most closely matches pip's behavior, but exposes the resolver to \"dependency confusion\" attacks whereby malicious actors can publish packages to public indexes with the same name as internal packages, causing the resolver to install the malicious package in lieu of the intended internal package.\n\nSee: ", + "description": "Search for every package name across all indexes, preferring the \"best\" version found. If a\npackage version is in multiple indexes, only look at the entry for the first index.\n\nIn this strategy, we look for every package across all indexes. When resolving, we consider\nall versions from all indexes, choosing the \"best\" version found (typically, the highest\ncompatible version).\n\nThis most closely matches pip's behavior, but exposes the resolver to \"dependency confusion\"\nattacks whereby malicious actors can publish packages to public indexes with the same name\nas internal packages, causing the resolver to install the malicious package in lieu of\nthe intended internal package.\n\nSee: ", "type": "string", - "enum": [ - "unsafe-best-match" - ] + "const": "unsafe-best-match" } ] }, @@ -1037,16 +1015,12 @@ { "description": "Do not use keyring for credential lookup.", "type": "string", - "enum": [ - "disabled" - ] + "const": "disabled" }, { "description": "Use the `keyring` command for credential lookup.", "type": "string", - "enum": [ - "subprocess" - ] + "const": "subprocess" } ] }, @@ -1055,30 +1029,22 @@ { "description": "Clone (i.e., copy-on-write) packages from the wheel into the `site-packages` directory.", "type": "string", - "enum": [ - "clone" - ] + "const": "clone" }, { "description": "Copy packages from the wheel into the `site-packages` directory.", "type": "string", - "enum": [ - "copy" - ] + "const": "copy" }, { "description": "Hard link packages from the wheel into the `site-packages` directory.", "type": "string", - "enum": [ - "hardlink" - ] + "const": "hardlink" }, { - "description": "Symbolically link packages from the wheel into the `site-packages` directory.\n\nWARNING: The use of symlinks is discouraged, as they create tight coupling between the cache and the target environment. For example, clearing the cache (`uv cache clear`) will break all installed packages by way of removing the underlying source files. Use symlinks with caution.", + "description": "Symbolically link packages from the wheel into the `site-packages` directory.\n\nWARNING: The use of symlinks is discouraged, as they create tight coupling between the\ncache and the target environment. For example, clearing the cache (`uv cache clear`) will\nbreak all installed packages by way of removing the underlying source files. Use symlinks\nwith caution.", "type": "string", - "enum": [ - "symlink" - ] + "const": "symlink" } ] }, @@ -1087,7 +1053,7 @@ "type": "string" }, "PackageName": { - "description": "The normalized name of a package.\n\nConverts the name to lowercase and collapses runs of `-`, `_`, and `.` down to a single `-`. For example, `---`, `.`, and `__` are all converted to a single `-`.\n\nSee: ", + "description": "The normalized name of a package.\n\nConverts the name to lowercase and collapses runs of `-`, `_`, and `.` down to a single `-`.\nFor example, `---`, `.`, and `__` are all converted to a single `-`.\n\nSee: ", "type": "string" }, "PackageNameSpecifier": { @@ -1096,11 +1062,8 @@ "pattern": "^(:none:|:all:|([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9]))$" }, "PipGroupName": { - "description": "The pip-compatible variant of a [`GroupName`].\n\nEither or :. If is omitted it defaults to \"pyproject.toml\".", + "description": "The pip-compatible variant of a [`GroupName`].\n\nEither or :.\nIf is omitted it defaults to \"pyproject.toml\".", "type": "object", - "required": [ - "name" - ], "properties": { "name": { "$ref": "#/definitions/GroupName" @@ -1111,10 +1074,13 @@ "null" ] } - } + }, + "required": [ + "name" + ] }, "PipOptions": { - "description": "Settings that are specific to the `uv pip` command-line interface.\n\nThese values will be ignored when running commands outside the `uv pip` namespace (e.g., `uv lock`, `uvx`).", + "description": "Settings that are specific to the `uv pip` command-line interface.\n\nThese values will be ignored when running commands outside the `uv pip` namespace (e.g.,\n`uv lock`, `uvx`).", "type": "object", "properties": { "all-extras": { @@ -1125,14 +1091,14 @@ ] }, "allow-empty-requirements": { - "description": "Allow `uv pip sync` with empty requirements, which will clear the environment of all packages.", + "description": "Allow `uv pip sync` with empty requirements, which will clear the environment of all\npackages.", "type": [ "boolean", "null" ] }, "annotation-style": { - "description": "The style of the annotation comments included in the output file, used to indicate the source of each package.", + "description": "The style of the annotation comments included in the output file, used to indicate the\nsource of each package.", "anyOf": [ { "$ref": "#/definitions/AnnotationStyle" @@ -1143,21 +1109,21 @@ ] }, "break-system-packages": { - "description": "Allow uv to modify an `EXTERNALLY-MANAGED` Python installation.\n\nWARNING: `--break-system-packages` is intended for use in continuous integration (CI) environments, when installing into Python installations that are managed by an external package manager, like `apt`. It should be used with caution, as such Python installations explicitly recommend against modifications by other package managers (like uv or pip).", + "description": "Allow uv to modify an `EXTERNALLY-MANAGED` Python installation.\n\nWARNING: `--break-system-packages` is intended for use in continuous integration (CI)\nenvironments, when installing into Python installations that are managed by an external\npackage manager, like `apt`. It should be used with caution, as such Python installations\nexplicitly recommend against modifications by other package managers (like uv or pip).", "type": [ "boolean", "null" ] }, "compile-bytecode": { - "description": "Compile Python files to bytecode after installation.\n\nBy default, uv does not compile Python (`.py`) files to bytecode (`__pycache__/*.pyc`); instead, compilation is performed lazily the first time a module is imported. For use-cases in which start time is critical, such as CLI applications and Docker containers, this option can be enabled to trade longer installation times for faster start times.\n\nWhen enabled, uv will process the entire site-packages directory (including packages that are not being modified by the current operation) for consistency. Like pip, it will also ignore errors.", + "description": "Compile Python files to bytecode after installation.\n\nBy default, uv does not compile Python (`.py`) files to bytecode (`__pycache__/*.pyc`);\ninstead, compilation is performed lazily the first time a module is imported. For use-cases\nin which start time is critical, such as CLI applications and Docker containers, this option\ncan be enabled to trade longer installation times for faster start times.\n\nWhen enabled, uv will process the entire site-packages directory (including packages that\nare not being modified by the current operation) for consistency. Like pip, it will also\nignore errors.", "type": [ "boolean", "null" ] }, "config-settings": { - "description": "Settings to pass to the [PEP 517](https://peps.python.org/pep-0517/) build backend, specified as `KEY=VALUE` pairs.", + "description": "Settings to pass to the [PEP 517](https://peps.python.org/pep-0517/) build backend,\nspecified as `KEY=VALUE` pairs.", "anyOf": [ { "$ref": "#/definitions/ConfigSettings" @@ -1175,7 +1141,7 @@ ] }, "dependency-metadata": { - "description": "Pre-defined static metadata for dependencies of the project (direct or transitive). When provided, enables the resolver to use the specified metadata instead of querying the registry or building the relevant package from source.\n\nMetadata should be provided in adherence with the [Metadata 2.3](https://packaging.python.org/en/latest/specifications/core-metadata/) standard, though only the following fields are respected:\n\n- `name`: The name of the package. - (Optional) `version`: The version of the package. If omitted, the metadata will be applied to all versions of the package. - (Optional) `requires-dist`: The dependencies of the package (e.g., `werkzeug>=0.14`). - (Optional) `requires-python`: The Python version required by the package (e.g., `>=3.10`). - (Optional) `provides-extras`: The extras provided by the package.", + "description": "Pre-defined static metadata for dependencies of the project (direct or transitive). When\nprovided, enables the resolver to use the specified metadata instead of querying the\nregistry or building the relevant package from source.\n\nMetadata should be provided in adherence with the [Metadata 2.3](https://packaging.python.org/en/latest/specifications/core-metadata/)\nstandard, though only the following fields are respected:\n\n- `name`: The name of the package.\n- (Optional) `version`: The version of the package. If omitted, the metadata will be applied\n to all versions of the package.\n- (Optional) `requires-dist`: The dependencies of the package (e.g., `werkzeug>=0.14`).\n- (Optional) `requires-python`: The Python version required by the package (e.g., `>=3.10`).\n- (Optional) `provides-extras`: The extras provided by the package.", "type": [ "array", "null" @@ -1199,7 +1165,7 @@ ] }, "emit-index-annotation": { - "description": "Include comment annotations indicating the index used to resolve each package (e.g., `# from https://pypi.org/simple`).", + "description": "Include comment annotations indicating the index used to resolve each package (e.g.,\n`# from https://pypi.org/simple`).", "type": [ "boolean", "null" @@ -1213,14 +1179,14 @@ ] }, "emit-marker-expression": { - "description": "Whether to emit a marker string indicating the conditions under which the set of pinned dependencies is valid.\n\nThe pinned dependencies may be valid even when the marker expression is false, but when the expression is true, the requirements are known to be correct.", + "description": "Whether to emit a marker string indicating the conditions under which the set of pinned\ndependencies is valid.\n\nThe pinned dependencies may be valid even when the marker expression is\nfalse, but when the expression is true, the requirements are known to\nbe correct.", "type": [ "boolean", "null" ] }, "exclude-newer": { - "description": "Limit candidate packages to those that were uploaded prior to a given point in time.\n\nAccepts a superset of [RFC 3339](https://www.rfc-editor.org/rfc/rfc3339.html) (e.g., `2006-12-02T02:07:43Z`). A full timestamp is required to ensure that the resolver will behave consistently across timezones.", + "description": "Limit candidate packages to those that were uploaded prior to a given point in time.\n\nAccepts a superset of [RFC 3339](https://www.rfc-editor.org/rfc/rfc3339.html) (e.g.,\n`2006-12-02T02:07:43Z`). A full timestamp is required to ensure that the resolver will\nbehave consistently across timezones.", "anyOf": [ { "$ref": "#/definitions/ExcludeNewer" @@ -1241,7 +1207,7 @@ } }, "extra-index-url": { - "description": "Extra URLs of package indexes to use, in addition to `--index-url`.\n\nAccepts either a repository compliant with [PEP 503](https://peps.python.org/pep-0503/) (the simple repository API), or a local directory laid out in the same format.\n\nAll indexes provided via this flag take priority over the index specified by [`index_url`](#index-url). When multiple indexes are provided, earlier values take priority.\n\nTo control uv's resolution strategy when multiple indexes are present, see [`index_strategy`](#index-strategy).", + "description": "Extra URLs of package indexes to use, in addition to `--index-url`.\n\nAccepts either a repository compliant with [PEP 503](https://peps.python.org/pep-0503/)\n(the simple repository API), or a local directory laid out in the same format.\n\nAll indexes provided via this flag take priority over the index specified by\n[`index_url`](#index-url). When multiple indexes are provided, earlier values take priority.\n\nTo control uv's resolution strategy when multiple indexes are present, see\n[`index_strategy`](#index-strategy).", "type": [ "array", "null" @@ -1251,7 +1217,7 @@ } }, "find-links": { - "description": "Locations to search for candidate distributions, in addition to those found in the registry indexes.\n\nIf a path, the target must be a directory that contains packages as wheel files (`.whl`) or source distributions (e.g., `.tar.gz` or `.zip`) at the top level.\n\nIf a URL, the page must contain a flat list of links to package files adhering to the formats described above.", + "description": "Locations to search for candidate distributions, in addition to those found in the registry\nindexes.\n\nIf a path, the target must be a directory that contains packages as wheel files (`.whl`) or\nsource distributions (e.g., `.tar.gz` or `.zip`) at the top level.\n\nIf a URL, the page must contain a flat list of links to package files adhering to the\nformats described above.", "type": [ "array", "null" @@ -1261,7 +1227,7 @@ } }, "fork-strategy": { - "description": "The strategy to use when selecting multiple versions of a given package across Python versions and platforms.\n\nBy default, uv will optimize for selecting the latest version of each package for each supported Python version (`requires-python`), while minimizing the number of selected versions across platforms.\n\nUnder `fewest`, uv will minimize the number of selected versions for each package, preferring older versions that are compatible with a wider range of supported Python versions or platforms.", + "description": "The strategy to use when selecting multiple versions of a given package across Python\nversions and platforms.\n\nBy default, uv will optimize for selecting the latest version of each package for each\nsupported Python version (`requires-python`), while minimizing the number of selected\nversions across platforms.\n\nUnder `fewest`, uv will minimize the number of selected versions for each package,\npreferring older versions that are compatible with a wider range of supported Python\nversions or platforms.", "anyOf": [ { "$ref": "#/definitions/ForkStrategy" @@ -1289,7 +1255,7 @@ } }, "index-strategy": { - "description": "The strategy to use when resolving against multiple index URLs.\n\nBy default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (`first-index`). This prevents \"dependency confusion\" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.", + "description": "The strategy to use when resolving against multiple index URLs.\n\nBy default, uv will stop at the first index on which a given package is available, and\nlimit resolutions to those present on that first index (`first-index`). This prevents\n\"dependency confusion\" attacks, whereby an attacker can upload a malicious package under the\nsame name to an alternate index.", "anyOf": [ { "$ref": "#/definitions/IndexStrategy" @@ -1300,7 +1266,7 @@ ] }, "index-url": { - "description": "The URL of the Python package index (by default: ).\n\nAccepts either a repository compliant with [PEP 503](https://peps.python.org/pep-0503/) (the simple repository API), or a local directory laid out in the same format.\n\nThe index provided by this setting is given lower priority than any indexes specified via [`extra_index_url`](#extra-index-url).", + "description": "The URL of the Python package index (by default: ).\n\nAccepts either a repository compliant with [PEP 503](https://peps.python.org/pep-0503/)\n(the simple repository API), or a local directory laid out in the same format.\n\nThe index provided by this setting is given lower priority than any indexes specified via\n[`extra_index_url`](#extra-index-url).", "anyOf": [ { "$ref": "#/definitions/IndexUrl" @@ -1311,7 +1277,7 @@ ] }, "keyring-provider": { - "description": "Attempt to use `keyring` for authentication for index URLs.\n\nAt present, only `--keyring-provider subprocess` is supported, which configures uv to use the `keyring` CLI to handle authentication.", + "description": "Attempt to use `keyring` for authentication for index URLs.\n\nAt present, only `--keyring-provider subprocess` is supported, which configures uv to\nuse the `keyring` CLI to handle authentication.", "anyOf": [ { "$ref": "#/definitions/KeyringProviderType" @@ -1322,7 +1288,7 @@ ] }, "link-mode": { - "description": "The method to use when installing packages from the global cache.\n\nDefaults to `clone` (also known as Copy-on-Write) on macOS, and `hardlink` on Linux and Windows.", + "description": "The method to use when installing packages from the global cache.\n\nDefaults to `clone` (also known as Copy-on-Write) on macOS, and `hardlink` on Linux and\nWindows.", "anyOf": [ { "$ref": "#/definitions/LinkMode" @@ -1333,14 +1299,14 @@ ] }, "no-annotate": { - "description": "Exclude comment annotations indicating the source of each package from the output file generated by `uv pip compile`.", + "description": "Exclude comment annotations indicating the source of each package from the output file\ngenerated by `uv pip compile`.", "type": [ "boolean", "null" ] }, "no-binary": { - "description": "Don't install pre-built wheels.\n\nThe given packages will be built and installed from source. The resolver will still use pre-built wheels to extract package metadata, if available.\n\nMultiple packages may be provided. Disable binaries for all packages with `:all:`. Clear previously specified packages with `:none:`.", + "description": "Don't install pre-built wheels.\n\nThe given packages will be built and installed from source. The resolver will still use\npre-built wheels to extract package metadata, if available.\n\nMultiple packages may be provided. Disable binaries for all packages with `:all:`.\nClear previously specified packages with `:none:`.", "type": [ "array", "null" @@ -1350,21 +1316,21 @@ } }, "no-build": { - "description": "Don't build source distributions.\n\nWhen enabled, resolving will not run arbitrary Python code. The cached wheels of already-built source distributions will be reused, but operations that require building distributions will exit with an error.\n\nAlias for `--only-binary :all:`.", + "description": "Don't build source distributions.\n\nWhen enabled, resolving will not run arbitrary Python code. The cached wheels of\nalready-built source distributions will be reused, but operations that require building\ndistributions will exit with an error.\n\nAlias for `--only-binary :all:`.", "type": [ "boolean", "null" ] }, "no-build-isolation": { - "description": "Disable isolation when building source distributions.\n\nAssumes that build dependencies specified by [PEP 518](https://peps.python.org/pep-0518/) are already installed.", + "description": "Disable isolation when building source distributions.\n\nAssumes that build dependencies specified by [PEP 518](https://peps.python.org/pep-0518/)\nare already installed.", "type": [ "boolean", "null" ] }, "no-build-isolation-package": { - "description": "Disable isolation when building source distributions for a specific package.\n\nAssumes that the packages' build dependencies specified by [PEP 518](https://peps.python.org/pep-0518/) are already installed.", + "description": "Disable isolation when building source distributions for a specific package.\n\nAssumes that the packages' build dependencies specified by [PEP 518](https://peps.python.org/pep-0518/)\nare already installed.", "type": [ "array", "null" @@ -1374,14 +1340,14 @@ } }, "no-deps": { - "description": "Ignore package dependencies, instead only add those packages explicitly listed on the command line to the resulting requirements file.", + "description": "Ignore package dependencies, instead only add those packages explicitly listed\non the command line to the resulting requirements file.", "type": [ "boolean", "null" ] }, "no-emit-package": { - "description": "Specify a package to omit from the output resolution. Its dependencies will still be included in the resolution. Equivalent to pip-compile's `--unsafe-package` option.", + "description": "Specify a package to omit from the output resolution. Its dependencies will still be\nincluded in the resolution. Equivalent to pip-compile's `--unsafe-package` option.", "type": [ "array", "null" @@ -1408,35 +1374,35 @@ ] }, "no-index": { - "description": "Ignore all registry indexes (e.g., PyPI), instead relying on direct URL dependencies and those provided via `--find-links`.", + "description": "Ignore all registry indexes (e.g., PyPI), instead relying on direct URL dependencies and\nthose provided via `--find-links`.", "type": [ "boolean", "null" ] }, "no-sources": { - "description": "Ignore the `tool.uv.sources` table when resolving dependencies. Used to lock against the standards-compliant, publishable package metadata, as opposed to using any local or Git sources.", + "description": "Ignore the `tool.uv.sources` table when resolving dependencies. Used to lock against the\nstandards-compliant, publishable package metadata, as opposed to using any local or Git\nsources.", "type": [ "boolean", "null" ] }, "no-strip-extras": { - "description": "Include extras in the output file.\n\nBy default, uv strips extras, as any packages pulled in by the extras are already included as dependencies in the output file directly. Further, output files generated with `--no-strip-extras` cannot be used as constraints files in `install` and `sync` invocations.", + "description": "Include extras in the output file.\n\nBy default, uv strips extras, as any packages pulled in by the extras are already included\nas dependencies in the output file directly. Further, output files generated with\n`--no-strip-extras` cannot be used as constraints files in `install` and `sync` invocations.", "type": [ "boolean", "null" ] }, "no-strip-markers": { - "description": "Include environment markers in the output file generated by `uv pip compile`.\n\nBy default, uv strips environment markers, as the resolution generated by `compile` is only guaranteed to be correct for the target environment.", + "description": "Include environment markers in the output file generated by `uv pip compile`.\n\nBy default, uv strips environment markers, as the resolution generated by `compile` is\nonly guaranteed to be correct for the target environment.", "type": [ "boolean", "null" ] }, "only-binary": { - "description": "Only use pre-built wheels; don't build source distributions.\n\nWhen enabled, resolving will not run code from the given packages. The cached wheels of already-built source distributions will be reused, but operations that require building distributions will exit with an error.\n\nMultiple packages may be provided. Disable binaries for all packages with `:all:`. Clear previously specified packages with `:none:`.", + "description": "Only use pre-built wheels; don't build source distributions.\n\nWhen enabled, resolving will not run code from the given packages. The cached wheels of already-built\nsource distributions will be reused, but operations that require building distributions will\nexit with an error.\n\nMultiple packages may be provided. Disable binaries for all packages with `:all:`.\nClear previously specified packages with `:none:`.", "type": [ "array", "null" @@ -1446,21 +1412,21 @@ } }, "output-file": { - "description": "Write the requirements generated by `uv pip compile` to the given `requirements.txt` file.\n\nIf the file already exists, the existing versions will be preferred when resolving dependencies, unless `--upgrade` is also specified.", + "description": "Write the requirements generated by `uv pip compile` to the given `requirements.txt` file.\n\nIf the file already exists, the existing versions will be preferred when resolving\ndependencies, unless `--upgrade` is also specified.", "type": [ "string", "null" ] }, "prefix": { - "description": "Install packages into `lib`, `bin`, and other top-level folders under the specified directory, as if a virtual environment were present at that location.\n\nIn general, prefer the use of `--python` to install into an alternate environment, as scripts and other artifacts installed via `--prefix` will reference the installing interpreter, rather than any interpreter added to the `--prefix` directory, rendering them non-portable.", + "description": "Install packages into `lib`, `bin`, and other top-level folders under the specified\ndirectory, as if a virtual environment were present at that location.\n\nIn general, prefer the use of `--python` to install into an alternate environment, as\nscripts and other artifacts installed via `--prefix` will reference the installing\ninterpreter, rather than any interpreter added to the `--prefix` directory, rendering them\nnon-portable.", "type": [ "string", "null" ] }, "prerelease": { - "description": "The strategy to use when considering pre-release versions.\n\nBy default, uv will accept pre-releases for packages that _only_ publish pre-releases, along with first-party requirements that contain an explicit pre-release marker in the declared specifiers (`if-necessary-or-explicit`).", + "description": "The strategy to use when considering pre-release versions.\n\nBy default, uv will accept pre-releases for packages that _only_ publish pre-releases,\nalong with first-party requirements that contain an explicit pre-release marker in the\ndeclared specifiers (`if-necessary-or-explicit`).", "anyOf": [ { "$ref": "#/definitions/PrereleaseMode" @@ -1471,14 +1437,14 @@ ] }, "python": { - "description": "The Python interpreter into which packages should be installed.\n\nBy default, uv installs into the virtual environment in the current working directory or any parent directory. The `--python` option allows you to specify a different interpreter, which is intended for use in continuous integration (CI) environments or other automated workflows.\n\nSupported formats: - `3.10` looks for an installed Python 3.10 in the registry on Windows (see `py --list-paths`), or `python3.10` on Linux and macOS. - `python3.10` or `python.exe` looks for a binary with the given name in `PATH`. - `/home/ferris/.local/bin/python3.10` uses the exact Python at the given path.", + "description": "The Python interpreter into which packages should be installed.\n\nBy default, uv installs into the virtual environment in the current working directory or\nany parent directory. The `--python` option allows you to specify a different interpreter,\nwhich is intended for use in continuous integration (CI) environments or other automated\nworkflows.\n\nSupported formats:\n- `3.10` looks for an installed Python 3.10 in the registry on Windows (see\n `py --list-paths`), or `python3.10` on Linux and macOS.\n- `python3.10` or `python.exe` looks for a binary with the given name in `PATH`.\n- `/home/ferris/.local/bin/python3.10` uses the exact Python at the given path.", "type": [ "string", "null" ] }, "python-platform": { - "description": "The platform for which requirements should be resolved.\n\nRepresented as a \"target triple\", a string that describes the target platform in terms of its CPU, vendor, and operating system name, like `x86_64-unknown-linux-gnu` or `aarch64-apple-darwin`.", + "description": "The platform for which requirements should be resolved.\n\nRepresented as a \"target triple\", a string that describes the target platform in terms of\nits CPU, vendor, and operating system name, like `x86_64-unknown-linux-gnu` or\n`aarch64-apple-darwin`.", "anyOf": [ { "$ref": "#/definitions/TargetTriple" @@ -1489,7 +1455,7 @@ ] }, "python-version": { - "description": "The minimum Python version that should be supported by the resolved requirements (e.g., `3.8` or `3.8.17`).\n\nIf a patch version is omitted, the minimum patch version is assumed. For example, `3.8` is mapped to `3.8.0`.", + "description": "The minimum Python version that should be supported by the resolved requirements (e.g.,\n`3.8` or `3.8.17`).\n\nIf a patch version is omitted, the minimum patch version is assumed. For example, `3.8` is\nmapped to `3.8.0`.", "anyOf": [ { "$ref": "#/definitions/PythonVersion" @@ -1507,7 +1473,7 @@ ] }, "reinstall-package": { - "description": "Reinstall a specific package, regardless of whether it's already installed. Implies `refresh-package`.", + "description": "Reinstall a specific package, regardless of whether it's already installed. Implies\n`refresh-package`.", "type": [ "array", "null" @@ -1517,14 +1483,14 @@ } }, "require-hashes": { - "description": "Require a matching hash for each requirement.\n\nHash-checking mode is all or nothing. If enabled, _all_ requirements must be provided with a corresponding hash or set of hashes. Additionally, if enabled, _all_ requirements must either be pinned to exact versions (e.g., `==1.0.0`), or be specified via direct URL.\n\nHash-checking mode introduces a number of additional constraints:\n\n- Git dependencies are not supported. - Editable installations are not supported. - Local dependencies are not supported, unless they point to a specific wheel (`.whl`) or source archive (`.zip`, `.tar.gz`), as opposed to a directory.", + "description": "Require a matching hash for each requirement.\n\nHash-checking mode is all or nothing. If enabled, _all_ requirements must be provided\nwith a corresponding hash or set of hashes. Additionally, if enabled, _all_ requirements\nmust either be pinned to exact versions (e.g., `==1.0.0`), or be specified via direct URL.\n\nHash-checking mode introduces a number of additional constraints:\n\n- Git dependencies are not supported.\n- Editable installations are not supported.\n- Local dependencies are not supported, unless they point to a specific wheel (`.whl`) or\n source archive (`.zip`, `.tar.gz`), as opposed to a directory.", "type": [ "boolean", "null" ] }, "resolution": { - "description": "The strategy to use when selecting between the different compatible versions for a given package requirement.\n\nBy default, uv will use the latest compatible version of each package (`highest`).", + "description": "The strategy to use when selecting between the different compatible versions for a given\npackage requirement.\n\nBy default, uv will use the latest compatible version of each package (`highest`).", "anyOf": [ { "$ref": "#/definitions/ResolutionMode" @@ -1535,28 +1501,28 @@ ] }, "strict": { - "description": "Validate the Python environment, to detect packages with missing dependencies and other issues.", + "description": "Validate the Python environment, to detect packages with missing dependencies and other\nissues.", "type": [ "boolean", "null" ] }, "system": { - "description": "Install packages into the system Python environment.\n\nBy default, uv installs into the virtual environment in the current working directory or any parent directory. The `--system` option instructs uv to instead use the first Python found in the system `PATH`.\n\nWARNING: `--system` is intended for use in continuous integration (CI) environments and should be used with caution, as it can modify the system Python installation.", + "description": "Install packages into the system Python environment.\n\nBy default, uv installs into the virtual environment in the current working directory or\nany parent directory. The `--system` option instructs uv to instead use the first Python\nfound in the system `PATH`.\n\nWARNING: `--system` is intended for use in continuous integration (CI) environments and\nshould be used with caution, as it can modify the system Python installation.", "type": [ "boolean", "null" ] }, "target": { - "description": "Install packages into the specified directory, rather than into the virtual or system Python environment. The packages will be installed at the top-level of the directory.", + "description": "Install packages into the specified directory, rather than into the virtual or system Python\nenvironment. The packages will be installed at the top-level of the directory.", "type": [ "string", "null" ] }, "torch-backend": { - "description": "The backend to use when fetching packages in the PyTorch ecosystem.\n\nWhen set, uv will ignore the configured index URLs for packages in the PyTorch ecosystem, and will instead use the defined backend.\n\nFor example, when set to `cpu`, uv will use the CPU-only PyTorch index; when set to `cu126`, uv will use the PyTorch index for CUDA 12.6.\n\nThe `auto` mode will attempt to detect the appropriate PyTorch index based on the currently installed CUDA drivers.\n\nThis option is in preview and may change in any future release.", + "description": "The backend to use when fetching packages in the PyTorch ecosystem.\n\nWhen set, uv will ignore the configured index URLs for packages in the PyTorch ecosystem,\nand will instead use the defined backend.\n\nFor example, when set to `cpu`, uv will use the CPU-only PyTorch index; when set to `cu126`,\nuv will use the PyTorch index for CUDA 12.6.\n\nThe `auto` mode will attempt to detect the appropriate PyTorch index based on the currently\ninstalled CUDA drivers.\n\nThis option is in preview and may change in any future release.", "anyOf": [ { "$ref": "#/definitions/TorchMode" @@ -1567,7 +1533,7 @@ ] }, "universal": { - "description": "Perform a universal resolution, attempting to generate a single `requirements.txt` output file that is compatible with all operating systems, architectures, and Python implementations.\n\nIn universal mode, the current Python version (or user-provided `--python-version`) will be treated as a lower bound. For example, `--universal --python-version 3.7` would produce a universal resolution for Python 3.7 and later.", + "description": "Perform a universal resolution, attempting to generate a single `requirements.txt` output\nfile that is compatible with all operating systems, architectures, and Python\nimplementations.\n\nIn universal mode, the current Python version (or user-provided `--python-version`) will be\ntreated as a lower bound. For example, `--universal --python-version 3.7` would produce a\nuniversal resolution for Python 3.7 and later.", "type": [ "boolean", "null" @@ -1581,7 +1547,7 @@ ] }, "upgrade-package": { - "description": "Allow upgrades for a specific package, ignoring pinned versions in any existing output file.\n\nAccepts both standalone package names (`ruff`) and version specifiers (`ruff<0.5.0`).", + "description": "Allow upgrades for a specific package, ignoring pinned versions in any existing output\nfile.\n\nAccepts both standalone package names (`ruff`) and version specifiers (`ruff<0.5.0`).", "type": [ "array", "null" @@ -1591,7 +1557,7 @@ } }, "verify-hashes": { - "description": "Validate any hashes provided in the requirements file.\n\nUnlike `--require-hashes`, `--verify-hashes` does not require that all requirements have hashes; instead, it will limit itself to verifying the hashes of those requirements that do include them.", + "description": "Validate any hashes provided in the requirements file.\n\nUnlike `--require-hashes`, `--verify-hashes` does not require that all requirements have\nhashes; instead, it will limit itself to verifying the hashes of those requirements that do\ninclude them.", "type": [ "boolean", "null" @@ -1600,42 +1566,35 @@ }, "additionalProperties": false }, + "PortablePathBuf": { + "type": "string" + }, "PrereleaseMode": { "oneOf": [ { "description": "Disallow all pre-release versions.", "type": "string", - "enum": [ - "disallow" - ] + "const": "disallow" }, { "description": "Allow all pre-release versions.", "type": "string", - "enum": [ - "allow" - ] + "const": "allow" }, { "description": "Allow pre-release versions if all versions of a package are pre-release.", "type": "string", - "enum": [ - "if-necessary" - ] + "const": "if-necessary" }, { - "description": "Allow pre-release versions for first-party packages with explicit pre-release markers in their version requirements.", + "description": "Allow pre-release versions for first-party packages with explicit pre-release markers in\ntheir version requirements.", "type": "string", - "enum": [ - "explicit" - ] + "const": "explicit" }, { - "description": "Allow pre-release versions if all versions of a package are pre-release, or if the package has an explicit pre-release marker in its version requirements.", + "description": "Allow pre-release versions if all versions of a package are pre-release, or if the package\nhas an explicit pre-release marker in its version requirements.", "type": "string", - "enum": [ - "if-necessary-or-explicit" - ] + "const": "if-necessary-or-explicit" } ] }, @@ -1644,23 +1603,17 @@ { "description": "Automatically download managed Python installations when needed.", "type": "string", - "enum": [ - "automatic" - ] + "const": "automatic" }, { "description": "Do not automatically download managed Python installations; require explicit installation.", "type": "string", - "enum": [ - "manual" - ] + "const": "manual" }, { "description": "Do not ever allow Python downloads.", "type": "string", - "enum": [ - "never" - ] + "const": "never" } ] }, @@ -1669,30 +1622,22 @@ { "description": "Only use managed Python installations; never use system Python installations.", "type": "string", - "enum": [ - "only-managed" - ] + "const": "only-managed" }, { - "description": "Prefer managed Python installations over system Python installations.\n\nSystem Python installations are still preferred over downloading managed Python versions. Use `only-managed` to always fetch a managed Python version.", + "description": "Prefer managed Python installations over system Python installations.\n\nSystem Python installations are still preferred over downloading managed Python versions.\nUse `only-managed` to always fetch a managed Python version.", "type": "string", - "enum": [ - "managed" - ] + "const": "managed" }, { "description": "Prefer system Python installations over managed Python installations.\n\nIf a system Python installation cannot be found, a managed Python installation can be used.", "type": "string", - "enum": [ - "system" - ] + "const": "system" }, { "description": "Only use system Python installations; never use managed Python installations.", "type": "string", - "enum": [ - "only-system" - ] + "const": "only-system" } ] }, @@ -1714,32 +1659,25 @@ { "description": "Resolve the highest compatible version of each package.", "type": "string", - "enum": [ - "highest" - ] + "const": "highest" }, { "description": "Resolve the lowest compatible version of each package.", "type": "string", - "enum": [ - "lowest" - ] + "const": "lowest" }, { - "description": "Resolve the lowest compatible version of any direct dependencies, and the highest compatible version of any transitive dependencies.", + "description": "Resolve the lowest compatible version of any direct dependencies, and the highest\ncompatible version of any transitive dependencies.", "type": "string", - "enum": [ - "lowest-direct" - ] + "const": "lowest-direct" } ] }, "SchemaConflictItem": { - "description": "A single item in a conflicting set.\n\nEach item is a pair of an (optional) package and a corresponding extra or group name for that package.", + "description": "A single item in a conflicting set.\n\nEach item is a pair of an (optional) package and a corresponding extra or group name for that\npackage.", "type": "object", "properties": { "extra": { - "default": null, "anyOf": [ { "$ref": "#/definitions/ExtraName" @@ -1747,10 +1685,10 @@ { "type": "null" } - ] + ], + "default": null }, "group": { - "default": null, "anyOf": [ { "$ref": "#/definitions/GroupName" @@ -1758,10 +1696,10 @@ { "type": "null" } - ] + ], + "default": null }, "package": { - "default": null, "anyOf": [ { "$ref": "#/definitions/PackageName" @@ -1769,33 +1707,34 @@ { "type": "null" } - ] + ], + "default": null } } }, "SchemaConflictSet": { - "description": "Like [`ConflictSet`], but for deserialization in `pyproject.toml`.\n\nThe schema format is different from the in-memory format. Specifically, the schema format does not allow specifying the package name (or will make it optional in the future), where as the in-memory format needs the package name.", + "description": "Like [`ConflictSet`], but for deserialization in `pyproject.toml`.\n\nThe schema format is different from the in-memory format. Specifically, the\nschema format does not allow specifying the package name (or will make it\noptional in the future), where as the in-memory format needs the package\nname.", "type": "array", "items": { "$ref": "#/definitions/SchemaConflictItem" } }, "SchemaConflicts": { - "description": "Like [`Conflicts`], but for deserialization in `pyproject.toml`.\n\nThe schema format is different from the in-memory format. Specifically, the schema format does not allow specifying the package name (or will make it optional in the future), where as the in-memory format needs the package name.\n\nN.B. `Conflicts` is still used for (de)serialization. Specifically, in the lock file, where the package name is required.", + "description": "Like [`Conflicts`], but for deserialization in `pyproject.toml`.\n\nThe schema format is different from the in-memory format. Specifically, the\nschema format does not allow specifying the package name (or will make it\noptional in the future), where as the in-memory format needs the package\nname.\n\nN.B. `Conflicts` is still used for (de)serialization. Specifically, in the\nlock file, where the package name is required.", "type": "array", "items": { "$ref": "#/definitions/SchemaConflictSet" } }, + "SerdePattern": { + "type": "string" + }, "Source": { "description": "A `tool.uv.sources` value.", "anyOf": [ { - "description": "A remote Git repository, available over HTTPS or SSH.\n\nExample: ```toml flask = { git = \"https://github.com/pallets/flask\", tag = \"3.0.0\" } ```", + "description": "A remote Git repository, available over HTTPS or SSH.\n\nExample:\n```toml\nflask = { git = \"https://github.com/pallets/flask\", tag = \"3.0.0\" }\n```", "type": "object", - "required": [ - "git" - ], "properties": { "branch": { "type": [ @@ -1815,8 +1754,11 @@ }, "git": { "description": "The repository URL (without the `git+` prefix).", - "type": "string", - "format": "uri" + "allOf": [ + { + "$ref": "#/definitions/DisplaySafeUrl" + } + ] }, "group": { "anyOf": [ @@ -1841,7 +1783,7 @@ "description": "The path to the directory with the `pyproject.toml`, if it's not in the archive root.", "anyOf": [ { - "$ref": "#/definitions/String" + "$ref": "#/definitions/PortablePathBuf" }, { "type": "null" @@ -1855,14 +1797,14 @@ ] } }, - "additionalProperties": false + "additionalProperties": false, + "required": [ + "git" + ] }, { - "description": "A remote `http://` or `https://` URL, either a wheel (`.whl`) or a source distribution (`.zip`, `.tar.gz`).\n\nExample: ```toml flask = { url = \"https://files.pythonhosted.org/packages/61/80/ffe1da13ad9300f87c93af113edd0638c75138c42a0994becfacac078c06/flask-3.0.3-py3-none-any.whl\" } ```", + "description": "A remote `http://` or `https://` URL, either a wheel (`.whl`) or a source distribution\n(`.zip`, `.tar.gz`).\n\nExample:\n```toml\nflask = { url = \"https://files.pythonhosted.org/packages/61/80/ffe1da13ad9300f87c93af113edd0638c75138c42a0994becfacac078c06/flask-3.0.3-py3-none-any.whl\" }\n```", "type": "object", - "required": [ - "url" - ], "properties": { "extra": { "anyOf": [ @@ -1888,10 +1830,10 @@ "$ref": "#/definitions/MarkerTree" }, "subdirectory": { - "description": "For source distributions, the path to the directory with the `pyproject.toml`, if it's not in the archive root.", + "description": "For source distributions, the path to the directory with the `pyproject.toml`, if it's\nnot in the archive root.", "anyOf": [ { - "$ref": "#/definitions/String" + "$ref": "#/definitions/PortablePathBuf" }, { "type": "null" @@ -1899,18 +1841,17 @@ ] }, "url": { - "type": "string", - "format": "uri" + "$ref": "#/definitions/DisplaySafeUrl" } }, - "additionalProperties": false + "additionalProperties": false, + "required": [ + "url" + ] }, { - "description": "The path to a dependency, either a wheel (a `.whl` file), source distribution (a `.zip` or `.tar.gz` file), or source tree (i.e., a directory containing a `pyproject.toml` or `setup.py` file in the root).", + "description": "The path to a dependency, either a wheel (a `.whl` file), source distribution (a `.zip` or\n`.tar.gz` file), or source tree (i.e., a directory containing a `pyproject.toml` or\n`setup.py` file in the root).", "type": "object", - "required": [ - "path" - ], "properties": { "editable": { "description": "`false` by default.", @@ -1943,24 +1884,24 @@ "$ref": "#/definitions/MarkerTree" }, "package": { - "description": "Whether to treat the dependency as a buildable Python package (`true`) or as a virtual package (`false`). If `false`, the package will not be built or installed, but its dependencies will be included in the virtual environment.\n\nWhen omitted, the package status is inferred based on the presence of a `[build-system]` in the project's `pyproject.toml`.", + "description": "Whether to treat the dependency as a buildable Python package (`true`) or as a virtual\npackage (`false`). If `false`, the package will not be built or installed, but its\ndependencies will be included in the virtual environment.\n\nWhen omitted, the package status is inferred based on the presence of a `[build-system]`\nin the project's `pyproject.toml`.", "type": [ "boolean", "null" ] }, "path": { - "$ref": "#/definitions/String" + "$ref": "#/definitions/PortablePathBuf" } }, - "additionalProperties": false + "additionalProperties": false, + "required": [ + "path" + ] }, { "description": "A dependency pinned to a specific index, e.g., `torch` after setting `torch` to `https://download.pytorch.org/whl/cu118`.", "type": "object", - "required": [ - "index" - ], "properties": { "extra": { "anyOf": [ @@ -1989,14 +1930,14 @@ "$ref": "#/definitions/MarkerTree" } }, - "additionalProperties": false + "additionalProperties": false, + "required": [ + "index" + ] }, { "description": "A dependency on another package in the workspace.", "type": "object", - "required": [ - "workspace" - ], "properties": { "extra": { "anyOf": [ @@ -2022,18 +1963,18 @@ "$ref": "#/definitions/MarkerTree" }, "workspace": { - "description": "When set to `false`, the package will be fetched from the remote index, rather than included as a workspace package.", + "description": "When set to `false`, the package will be fetched from the remote index, rather than\nincluded as a workspace package.", "type": "boolean" } }, - "additionalProperties": false + "additionalProperties": false, + "required": [ + "workspace" + ] } ] }, "Sources": { - "$ref": "#/definitions/SourcesWire" - }, - "SourcesWire": { "anyOf": [ { "$ref": "#/definitions/Source" @@ -2047,25 +1988,22 @@ ] }, "StaticMetadata": { - "description": "A subset of the Python Package Metadata 2.3 standard as specified in .", + "description": "A subset of the Python Package Metadata 2.3 standard as specified in\n.", "type": "object", - "required": [ - "name" - ], "properties": { "name": { "$ref": "#/definitions/PackageName" }, "provides-extras": { - "default": [], "type": "array", + "default": [], "items": { "$ref": "#/definitions/ExtraName" } }, "requires-dist": { - "default": [], "type": "array", + "default": [], "items": { "$ref": "#/definitions/Requirement" } @@ -2084,286 +2022,209 @@ "null" ] } - } + }, + "required": [ + "name" + ] }, "StatusCode": { "description": "HTTP status code (100-599)", - "type": "integer", - "format": "uint16", - "maximum": 599.0, - "minimum": 100.0 - }, - "String": { - "type": "string" + "type": "number", + "maximum": 599, + "minimum": 100 }, "TargetTriple": { - "description": "The supported target triples. Each triple consists of an architecture, vendor, and operating system.\n\nSee: ", + "description": "The supported target triples. Each triple consists of an architecture, vendor, and operating\nsystem.\n\nSee: ", "oneOf": [ { "description": "An alias for `x86_64-pc-windows-msvc`, the default target for Windows.", "type": "string", - "enum": [ - "windows" - ] + "const": "windows" }, { "description": "An alias for `x86_64-unknown-linux-gnu`, the default target for Linux.", "type": "string", - "enum": [ - "linux" - ] + "const": "linux" }, { "description": "An alias for `aarch64-apple-darwin`, the default target for macOS.", "type": "string", - "enum": [ - "macos" - ] + "const": "macos" }, { "description": "A 64-bit x86 Windows target.", "type": "string", - "enum": [ - "x86_64-pc-windows-msvc" - ] + "const": "x86_64-pc-windows-msvc" }, { "description": "A 32-bit x86 Windows target.", "type": "string", - "enum": [ - "i686-pc-windows-msvc" - ] + "const": "i686-pc-windows-msvc" }, { "description": "An x86 Linux target. Equivalent to `x86_64-manylinux_2_17`.", "type": "string", - "enum": [ - "x86_64-unknown-linux-gnu" - ] + "const": "x86_64-unknown-linux-gnu" }, { - "description": "An ARM-based macOS target, as seen on Apple Silicon devices\n\nBy default, assumes the least-recent, non-EOL macOS version (13.0), but respects the `MACOSX_DEPLOYMENT_TARGET` environment variable if set.", + "description": "An ARM-based macOS target, as seen on Apple Silicon devices\n\nBy default, assumes the least-recent, non-EOL macOS version (13.0), but respects\nthe `MACOSX_DEPLOYMENT_TARGET` environment variable if set.", "type": "string", - "enum": [ - "aarch64-apple-darwin" - ] + "const": "aarch64-apple-darwin" }, { - "description": "An x86 macOS target.\n\nBy default, assumes the least-recent, non-EOL macOS version (13.0), but respects the `MACOSX_DEPLOYMENT_TARGET` environment variable if set.", + "description": "An x86 macOS target.\n\nBy default, assumes the least-recent, non-EOL macOS version (13.0), but respects\nthe `MACOSX_DEPLOYMENT_TARGET` environment variable if set.", "type": "string", - "enum": [ - "x86_64-apple-darwin" - ] + "const": "x86_64-apple-darwin" }, { "description": "An ARM64 Linux target. Equivalent to `aarch64-manylinux_2_17`.", "type": "string", - "enum": [ - "aarch64-unknown-linux-gnu" - ] + "const": "aarch64-unknown-linux-gnu" }, { "description": "An ARM64 Linux target.", "type": "string", - "enum": [ - "aarch64-unknown-linux-musl" - ] + "const": "aarch64-unknown-linux-musl" }, { "description": "An `x86_64` Linux target.", "type": "string", - "enum": [ - "x86_64-unknown-linux-musl" - ] + "const": "x86_64-unknown-linux-musl" }, { "description": "An `x86_64` target for the `manylinux2014` platform. Equivalent to `x86_64-manylinux_2_17`.", "type": "string", - "enum": [ - "x86_64-manylinux2014" - ] + "const": "x86_64-manylinux2014" }, { "description": "An `x86_64` target for the `manylinux_2_17` platform.", "type": "string", - "enum": [ - "x86_64-manylinux_2_17" - ] + "const": "x86_64-manylinux_2_17" }, { "description": "An `x86_64` target for the `manylinux_2_28` platform.", "type": "string", - "enum": [ - "x86_64-manylinux_2_28" - ] + "const": "x86_64-manylinux_2_28" }, { "description": "An `x86_64` target for the `manylinux_2_31` platform.", "type": "string", - "enum": [ - "x86_64-manylinux_2_31" - ] + "const": "x86_64-manylinux_2_31" }, { "description": "An `x86_64` target for the `manylinux_2_32` platform.", "type": "string", - "enum": [ - "x86_64-manylinux_2_32" - ] + "const": "x86_64-manylinux_2_32" }, { "description": "An `x86_64` target for the `manylinux_2_33` platform.", "type": "string", - "enum": [ - "x86_64-manylinux_2_33" - ] + "const": "x86_64-manylinux_2_33" }, { "description": "An `x86_64` target for the `manylinux_2_34` platform.", "type": "string", - "enum": [ - "x86_64-manylinux_2_34" - ] + "const": "x86_64-manylinux_2_34" }, { "description": "An `x86_64` target for the `manylinux_2_35` platform.", "type": "string", - "enum": [ - "x86_64-manylinux_2_35" - ] + "const": "x86_64-manylinux_2_35" }, { "description": "An `x86_64` target for the `manylinux_2_36` platform.", "type": "string", - "enum": [ - "x86_64-manylinux_2_36" - ] + "const": "x86_64-manylinux_2_36" }, { "description": "An `x86_64` target for the `manylinux_2_37` platform.", "type": "string", - "enum": [ - "x86_64-manylinux_2_37" - ] + "const": "x86_64-manylinux_2_37" }, { "description": "An `x86_64` target for the `manylinux_2_38` platform.", "type": "string", - "enum": [ - "x86_64-manylinux_2_38" - ] + "const": "x86_64-manylinux_2_38" }, { "description": "An `x86_64` target for the `manylinux_2_39` platform.", "type": "string", - "enum": [ - "x86_64-manylinux_2_39" - ] + "const": "x86_64-manylinux_2_39" }, { "description": "An `x86_64` target for the `manylinux_2_40` platform.", "type": "string", - "enum": [ - "x86_64-manylinux_2_40" - ] + "const": "x86_64-manylinux_2_40" }, { "description": "An ARM64 target for the `manylinux2014` platform. Equivalent to `aarch64-manylinux_2_17`.", "type": "string", - "enum": [ - "aarch64-manylinux2014" - ] + "const": "aarch64-manylinux2014" }, { "description": "An ARM64 target for the `manylinux_2_17` platform.", "type": "string", - "enum": [ - "aarch64-manylinux_2_17" - ] + "const": "aarch64-manylinux_2_17" }, { "description": "An ARM64 target for the `manylinux_2_28` platform.", "type": "string", - "enum": [ - "aarch64-manylinux_2_28" - ] + "const": "aarch64-manylinux_2_28" }, { "description": "An ARM64 target for the `manylinux_2_31` platform.", "type": "string", - "enum": [ - "aarch64-manylinux_2_31" - ] + "const": "aarch64-manylinux_2_31" }, { "description": "An ARM64 target for the `manylinux_2_32` platform.", "type": "string", - "enum": [ - "aarch64-manylinux_2_32" - ] + "const": "aarch64-manylinux_2_32" }, { "description": "An ARM64 target for the `manylinux_2_33` platform.", "type": "string", - "enum": [ - "aarch64-manylinux_2_33" - ] + "const": "aarch64-manylinux_2_33" }, { "description": "An ARM64 target for the `manylinux_2_34` platform.", "type": "string", - "enum": [ - "aarch64-manylinux_2_34" - ] + "const": "aarch64-manylinux_2_34" }, { "description": "An ARM64 target for the `manylinux_2_35` platform.", "type": "string", - "enum": [ - "aarch64-manylinux_2_35" - ] + "const": "aarch64-manylinux_2_35" }, { "description": "An ARM64 target for the `manylinux_2_36` platform.", "type": "string", - "enum": [ - "aarch64-manylinux_2_36" - ] + "const": "aarch64-manylinux_2_36" }, { "description": "An ARM64 target for the `manylinux_2_37` platform.", "type": "string", - "enum": [ - "aarch64-manylinux_2_37" - ] + "const": "aarch64-manylinux_2_37" }, { "description": "An ARM64 target for the `manylinux_2_38` platform.", "type": "string", - "enum": [ - "aarch64-manylinux_2_38" - ] + "const": "aarch64-manylinux_2_38" }, { "description": "An ARM64 target for the `manylinux_2_39` platform.", "type": "string", - "enum": [ - "aarch64-manylinux_2_39" - ] + "const": "aarch64-manylinux_2_39" }, { "description": "An ARM64 target for the `manylinux_2_40` platform.", "type": "string", - "enum": [ - "aarch64-manylinux_2_40" - ] + "const": "aarch64-manylinux_2_40" }, { "description": "A wasm32 target using the the Pyodide 2024 platform. Meant for use with Python 3.12.", "type": "string", - "enum": [ - "wasm32-pyodide2024" - ] + "const": "wasm32-pyodide2024" } ] }, @@ -2383,13 +2244,13 @@ "type": "object", "properties": { "exclude": { - "description": "Packages to exclude as workspace members. If a package matches both `members` and `exclude`, it will be excluded.\n\nSupports both globs and explicit paths.\n\nFor more information on the glob syntax, refer to the [`glob` documentation](https://docs.rs/glob/latest/glob/struct.Pattern.html).", + "description": "Packages to exclude as workspace members. If a package matches both `members` and\n`exclude`, it will be excluded.\n\nSupports both globs and explicit paths.\n\nFor more information on the glob syntax, refer to the [`glob` documentation](https://docs.rs/glob/latest/glob/struct.Pattern.html).", "type": [ "array", "null" ], "items": { - "$ref": "#/definitions/String" + "$ref": "#/definitions/SerdePattern" } }, "members": { @@ -2399,7 +2260,7 @@ "null" ], "items": { - "$ref": "#/definitions/String" + "$ref": "#/definitions/SerdePattern" } } }, @@ -2411,303 +2272,217 @@ { "description": "Select the appropriate PyTorch index based on the operating system and CUDA driver version.", "type": "string", - "enum": [ - "auto" - ] + "const": "auto" }, { "description": "Use the CPU-only PyTorch index.", "type": "string", - "enum": [ - "cpu" - ] + "const": "cpu" }, { "description": "Use the PyTorch index for CUDA 12.8.", "type": "string", - "enum": [ - "cu128" - ] + "const": "cu128" }, { "description": "Use the PyTorch index for CUDA 12.6.", "type": "string", - "enum": [ - "cu126" - ] + "const": "cu126" }, { "description": "Use the PyTorch index for CUDA 12.5.", "type": "string", - "enum": [ - "cu125" - ] + "const": "cu125" }, { "description": "Use the PyTorch index for CUDA 12.4.", "type": "string", - "enum": [ - "cu124" - ] + "const": "cu124" }, { "description": "Use the PyTorch index for CUDA 12.3.", "type": "string", - "enum": [ - "cu123" - ] + "const": "cu123" }, { "description": "Use the PyTorch index for CUDA 12.2.", "type": "string", - "enum": [ - "cu122" - ] + "const": "cu122" }, { "description": "Use the PyTorch index for CUDA 12.1.", "type": "string", - "enum": [ - "cu121" - ] + "const": "cu121" }, { "description": "Use the PyTorch index for CUDA 12.0.", "type": "string", - "enum": [ - "cu120" - ] + "const": "cu120" }, { "description": "Use the PyTorch index for CUDA 11.8.", "type": "string", - "enum": [ - "cu118" - ] + "const": "cu118" }, { "description": "Use the PyTorch index for CUDA 11.7.", "type": "string", - "enum": [ - "cu117" - ] + "const": "cu117" }, { "description": "Use the PyTorch index for CUDA 11.6.", "type": "string", - "enum": [ - "cu116" - ] + "const": "cu116" }, { "description": "Use the PyTorch index for CUDA 11.5.", "type": "string", - "enum": [ - "cu115" - ] + "const": "cu115" }, { "description": "Use the PyTorch index for CUDA 11.4.", "type": "string", - "enum": [ - "cu114" - ] + "const": "cu114" }, { "description": "Use the PyTorch index for CUDA 11.3.", "type": "string", - "enum": [ - "cu113" - ] + "const": "cu113" }, { "description": "Use the PyTorch index for CUDA 11.2.", "type": "string", - "enum": [ - "cu112" - ] + "const": "cu112" }, { "description": "Use the PyTorch index for CUDA 11.1.", "type": "string", - "enum": [ - "cu111" - ] + "const": "cu111" }, { "description": "Use the PyTorch index for CUDA 11.0.", "type": "string", - "enum": [ - "cu110" - ] + "const": "cu110" }, { "description": "Use the PyTorch index for CUDA 10.2.", "type": "string", - "enum": [ - "cu102" - ] + "const": "cu102" }, { "description": "Use the PyTorch index for CUDA 10.1.", "type": "string", - "enum": [ - "cu101" - ] + "const": "cu101" }, { "description": "Use the PyTorch index for CUDA 10.0.", "type": "string", - "enum": [ - "cu100" - ] + "const": "cu100" }, { "description": "Use the PyTorch index for CUDA 9.2.", "type": "string", - "enum": [ - "cu92" - ] + "const": "cu92" }, { "description": "Use the PyTorch index for CUDA 9.1.", "type": "string", - "enum": [ - "cu91" - ] + "const": "cu91" }, { "description": "Use the PyTorch index for CUDA 9.0.", "type": "string", - "enum": [ - "cu90" - ] + "const": "cu90" }, { "description": "Use the PyTorch index for CUDA 8.0.", "type": "string", - "enum": [ - "cu80" - ] + "const": "cu80" }, { "description": "Use the PyTorch index for ROCm 6.3.", "type": "string", - "enum": [ - "rocm6.3" - ] + "const": "rocm6.3" }, { "description": "Use the PyTorch index for ROCm 6.2.4.", "type": "string", - "enum": [ - "rocm6.2.4" - ] + "const": "rocm6.2.4" }, { "description": "Use the PyTorch index for ROCm 6.2.", "type": "string", - "enum": [ - "rocm6.2" - ] + "const": "rocm6.2" }, { "description": "Use the PyTorch index for ROCm 6.1.", "type": "string", - "enum": [ - "rocm6.1" - ] + "const": "rocm6.1" }, { "description": "Use the PyTorch index for ROCm 6.0.", "type": "string", - "enum": [ - "rocm6.0" - ] + "const": "rocm6.0" }, { "description": "Use the PyTorch index for ROCm 5.7.", "type": "string", - "enum": [ - "rocm5.7" - ] + "const": "rocm5.7" }, { "description": "Use the PyTorch index for ROCm 5.6.", "type": "string", - "enum": [ - "rocm5.6" - ] + "const": "rocm5.6" }, { "description": "Use the PyTorch index for ROCm 5.5.", "type": "string", - "enum": [ - "rocm5.5" - ] + "const": "rocm5.5" }, { "description": "Use the PyTorch index for ROCm 5.4.2.", "type": "string", - "enum": [ - "rocm5.4.2" - ] + "const": "rocm5.4.2" }, { "description": "Use the PyTorch index for ROCm 5.4.", "type": "string", - "enum": [ - "rocm5.4" - ] + "const": "rocm5.4" }, { "description": "Use the PyTorch index for ROCm 5.3.", "type": "string", - "enum": [ - "rocm5.3" - ] + "const": "rocm5.3" }, { "description": "Use the PyTorch index for ROCm 5.2.", "type": "string", - "enum": [ - "rocm5.2" - ] + "const": "rocm5.2" }, { "description": "Use the PyTorch index for ROCm 5.1.1.", "type": "string", - "enum": [ - "rocm5.1.1" - ] + "const": "rocm5.1.1" }, { "description": "Use the PyTorch index for ROCm 4.2.", "type": "string", - "enum": [ - "rocm4.2" - ] + "const": "rocm4.2" }, { "description": "Use the PyTorch index for ROCm 4.1.", "type": "string", - "enum": [ - "rocm4.1" - ] + "const": "rocm4.1" }, { "description": "Use the PyTorch index for ROCm 4.0.1.", "type": "string", - "enum": [ - "rocm4.0.1" - ] + "const": "rocm4.0.1" }, { "description": "Use the PyTorch index for Intel XPU.", "type": "string", - "enum": [ - "xpu" - ] + "const": "xpu" } ] }, @@ -2727,9 +2502,7 @@ { "description": "Try trusted publishing when we're already in GitHub Actions, continue if that fails.", "type": "string", - "enum": [ - "automatic" - ] + "const": "automatic" } ] }, @@ -2738,39 +2511,39 @@ "type": "object", "properties": { "data": { - "default": null, "type": [ "string", "null" - ] + ], + "default": null }, "headers": { - "default": null, "type": [ "string", "null" - ] + ], + "default": null }, "platlib": { - "default": null, "type": [ "string", "null" - ] + ], + "default": null }, "purelib": { - "default": null, "type": [ "string", "null" - ] + ], + "default": null }, "scripts": { - "default": null, "type": [ "string", "null" - ] + ], + "default": null } }, "additionalProperties": false From 283323a78acae50e5a01f4a5754dd1fa422b5a4a Mon Sep 17 00:00:00 2001 From: konsti Date: Wed, 25 Jun 2025 09:44:22 +0200 Subject: [PATCH 084/185] Allow symlinks in the build backend (#14212) In workspaces with multiple packages, you usually don't want to include shared files such as the license repeatedly. Instead, we reading from symlinked files. This would be supported if we had used std's `is_file` and read methods, but walkdir's `is_file` does not consider symlinked files as files. See https://github.com/astral-sh/uv/issues/3957#issuecomment-2994675003 --- crates/uv-build-backend/src/lib.rs | 14 +++- crates/uv-build-backend/src/source_dist.rs | 30 ++------ crates/uv-build-backend/src/wheel.rs | 36 ++------- crates/uv/tests/it/build_backend.rs | 90 ++++++++++++++++++++++ 4 files changed, 115 insertions(+), 55 deletions(-) diff --git a/crates/uv-build-backend/src/lib.rs b/crates/uv-build-backend/src/lib.rs index 15ff81a4b..548214c32 100644 --- a/crates/uv-build-backend/src/lib.rs +++ b/crates/uv-build-backend/src/lib.rs @@ -9,12 +9,12 @@ pub use settings::{BuildBackendSettings, WheelDataIncludes}; pub use source_dist::{build_source_dist, list_source_dist}; pub use wheel::{build_editable, build_wheel, list_wheel, metadata}; -use std::fs::FileType; use std::io; use std::path::{Path, PathBuf}; use std::str::FromStr; use thiserror::Error; use tracing::debug; +use walkdir::DirEntry; use uv_fs::Simplified; use uv_globfilter::PortableGlobError; @@ -54,8 +54,6 @@ pub enum Error { #[source] err: walkdir::Error, }, - #[error("Unsupported file type {:?}: `{}`", _1, _0.user_display())] - UnsupportedFileType(PathBuf, FileType), #[error("Failed to write wheel zip archive")] Zip(#[from] zip::result::ZipError), #[error("Failed to write RECORD file")] @@ -86,6 +84,16 @@ trait DirectoryWriter { /// Files added through the method are considered generated when listing included files. fn write_bytes(&mut self, path: &str, bytes: &[u8]) -> Result<(), Error>; + /// Add the file or directory to the path. + fn write_dir_entry(&mut self, entry: &DirEntry, target_path: &str) -> Result<(), Error> { + if entry.file_type().is_dir() { + self.write_directory(target_path)?; + } else { + self.write_file(target_path, entry.path())?; + } + Ok(()) + } + /// Add a local file. fn write_file(&mut self, path: &str, file: &Path) -> Result<(), Error>; diff --git a/crates/uv-build-backend/src/source_dist.rs b/crates/uv-build-backend/src/source_dist.rs index 6285ae7c0..0a302ccf2 100644 --- a/crates/uv-build-backend/src/source_dist.rs +++ b/crates/uv-build-backend/src/source_dist.rs @@ -250,32 +250,16 @@ fn write_source_dist( .expect("walkdir starts with root"); if !include_matcher.match_path(relative) || exclude_matcher.is_match(relative) { - trace!("Excluding: `{}`", relative.user_display()); + trace!("Excluding from sdist: `{}`", relative.user_display()); continue; } - debug!("Including {}", relative.user_display()); - if entry.file_type().is_dir() { - writer.write_directory( - &Path::new(&top_level) - .join(relative) - .portable_display() - .to_string(), - )?; - } else if entry.file_type().is_file() { - writer.write_file( - &Path::new(&top_level) - .join(relative) - .portable_display() - .to_string(), - entry.path(), - )?; - } else { - return Err(Error::UnsupportedFileType( - relative.to_path_buf(), - entry.file_type(), - )); - } + let entry_path = Path::new(&top_level) + .join(relative) + .portable_display() + .to_string(); + debug!("Adding to sdist: {}", relative.user_display()); + writer.write_dir_entry(&entry, &entry_path)?; } debug!("Visited {files_visited} files for source dist build"); diff --git a/crates/uv-build-backend/src/wheel.rs b/crates/uv-build-backend/src/wheel.rs index da376d078..7da232941 100644 --- a/crates/uv-build-backend/src/wheel.rs +++ b/crates/uv-build-backend/src/wheel.rs @@ -164,7 +164,7 @@ fn write_wheel( .path() .strip_prefix(source_tree) .expect("walkdir starts with root"); - let wheel_path = entry + let entry_path = entry .path() .strip_prefix(&src_root) .expect("walkdir starts with root"); @@ -172,21 +172,10 @@ fn write_wheel( trace!("Excluding from module: `{}`", match_path.user_display()); continue; } - let wheel_path = wheel_path.portable_display().to_string(); - debug!("Adding to wheel: `{wheel_path}`"); - - if entry.file_type().is_dir() { - wheel_writer.write_directory(&wheel_path)?; - } else if entry.file_type().is_file() { - wheel_writer.write_file(&wheel_path, entry.path())?; - } else { - // TODO(konsti): We may want to support symlinks, there is support for installing them. - return Err(Error::UnsupportedFileType( - entry.path().to_path_buf(), - entry.file_type(), - )); - } + let entry_path = entry_path.portable_display().to_string(); + debug!("Adding to wheel: {entry_path}"); + wheel_writer.write_dir_entry(&entry, &entry_path)?; } debug!("Visited {files_visited} files for wheel build"); @@ -519,23 +508,12 @@ fn wheel_subdir_from_globs( continue; } - let relative_licenses = Path::new(target) + let license_path = Path::new(target) .join(relative) .portable_display() .to_string(); - - if entry.file_type().is_dir() { - wheel_writer.write_directory(&relative_licenses)?; - } else if entry.file_type().is_file() { - debug!("Adding {} file: `{}`", globs_field, relative.user_display()); - wheel_writer.write_file(&relative_licenses, entry.path())?; - } else { - // TODO(konsti): We may want to support symlinks, there is support for installing them. - return Err(Error::UnsupportedFileType( - entry.path().to_path_buf(), - entry.file_type(), - )); - } + debug!("Adding for {}: `{}`", globs_field, relative.user_display()); + wheel_writer.write_dir_entry(&entry, &license_path)?; } Ok(()) } diff --git a/crates/uv/tests/it/build_backend.rs b/crates/uv/tests/it/build_backend.rs index c2d99ba3e..3dd38278f 100644 --- a/crates/uv/tests/it/build_backend.rs +++ b/crates/uv/tests/it/build_backend.rs @@ -768,3 +768,93 @@ fn complex_namespace_packages() -> Result<()> { ); Ok(()) } + +/// Test that a symlinked file (here: license) gets included. +#[test] +#[cfg(unix)] +fn symlinked_file() -> Result<()> { + let context = TestContext::new("3.12"); + + let project = context.temp_dir.child("project"); + context + .init() + .arg("--preview") + .arg("--build-backend") + .arg("uv") + .arg(project.path()) + .assert() + .success(); + + project.child("pyproject.toml").write_str(indoc! {r#" + [project] + name = "project" + version = "1.0.0" + license-files = ["LICENSE"] + + [build-system] + requires = ["uv_build>=0.5.15,<10000"] + build-backend = "uv_build" + "# + })?; + + let license_file = context.temp_dir.child("LICENSE"); + let license_symlink = project.child("LICENSE"); + + let license_text = "Project license"; + license_file.write_str(license_text)?; + fs_err::os::unix::fs::symlink(license_file.path(), license_symlink.path())?; + + uv_snapshot!(context + .build_backend() + .arg("build-sdist") + .arg(context.temp_dir.path()) + .current_dir(project.path()), @r" + success: true + exit_code: 0 + ----- stdout ----- + project-1.0.0.tar.gz + + ----- stderr ----- + "); + + uv_snapshot!(context + .build_backend() + .arg("build-wheel") + .arg(context.temp_dir.path()) + .current_dir(project.path()), @r" + success: true + exit_code: 0 + ----- stdout ----- + project-1.0.0-py3-none-any.whl + + ----- stderr ----- + "); + + uv_snapshot!(context.filters(), context.pip_install().arg("project-1.0.0-py3-none-any.whl"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + project==1.0.0 (from file://[TEMP_DIR]/project-1.0.0-py3-none-any.whl) + "); + + // Check that we included the actual license text and not a broken symlink. + let installed_license = context + .site_packages() + .join("project-1.0.0.dist-info") + .join("licenses") + .join("LICENSE"); + assert!( + fs_err::symlink_metadata(&installed_license)? + .file_type() + .is_file() + ); + let license = fs_err::read_to_string(&installed_license)?; + assert_eq!(license, license_text); + + Ok(()) +} From 5b2c3595a7afe4539a41702aaaf0409fe571f3ff Mon Sep 17 00:00:00 2001 From: John Mumm Date: Wed, 25 Jun 2025 04:02:06 -0400 Subject: [PATCH 085/185] Require disambiguated relative paths for `--index` (#14152) We do not currently support passing index names to `--index` for installing packages. However, we do accept relative paths that can look like index names. This PR adds the requirement that `--index` values must be disambiguated with a prefix (`./` or `../` on Unix and Windows or `.\\` or `..\\` on Windows). For now, if an ambiguous value is provided, uv will warn that this will not be supported in the future. Currently, if you provide an index name like `--index test` when there is no `test` directory, uv will error with a `Directory not found...` error. That's not very informative if you thought index names were supported. The new warning makes the context clearer. Closes #13921 --- Cargo.lock | 1 + crates/uv-cli/src/lib.rs | 3 + crates/uv-distribution-types/Cargo.toml | 1 + crates/uv-distribution-types/src/index_url.rs | 85 +++++++++++++++++++ crates/uv/src/settings.rs | 6 ++ crates/uv/tests/it/edit.rs | 52 +++++++++++- docs/reference/cli.md | 18 ++++ 7 files changed, 162 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2e1957e9f..781f98079 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5177,6 +5177,7 @@ dependencies = [ "uv-pypi-types", "uv-redacted", "uv-small-str", + "uv-warnings", "version-ranges", ] diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index e58f6c079..bf605198f 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -5130,6 +5130,9 @@ pub struct IndexArgs { /// All indexes provided via this flag take priority over the index specified by /// `--default-index` (which defaults to PyPI). When multiple `--index` flags are provided, /// earlier values take priority. + /// + /// Index names are not supported as values. Relative paths must be disambiguated from index + /// names with `./` or `../` on Unix or `.\\`, `..\\`, `./` or `../` on Windows. // // The nested Vec structure (`Vec>>`) is required for clap's // value parsing mechanism, which processes one value at a time, in order to handle diff --git a/crates/uv-distribution-types/Cargo.toml b/crates/uv-distribution-types/Cargo.toml index dc5a70166..1ca28c5ed 100644 --- a/crates/uv-distribution-types/Cargo.toml +++ b/crates/uv-distribution-types/Cargo.toml @@ -29,6 +29,7 @@ uv-platform-tags = { workspace = true } uv-pypi-types = { workspace = true } uv-redacted = { workspace = true } uv-small-str = { workspace = true } +uv-warnings = { workspace = true } arcstr = { workspace = true } bitflags = { workspace = true } diff --git a/crates/uv-distribution-types/src/index_url.rs b/crates/uv-distribution-types/src/index_url.rs index 9d07c9cbe..2b686c9fe 100644 --- a/crates/uv-distribution-types/src/index_url.rs +++ b/crates/uv-distribution-types/src/index_url.rs @@ -12,6 +12,7 @@ use url::{ParseError, Url}; use uv_pep508::{Scheme, VerbatimUrl, VerbatimUrlError, split_scheme}; use uv_redacted::DisplaySafeUrl; +use uv_warnings::warn_user; use crate::{Index, IndexStatusCodeStrategy, Verbatim}; @@ -135,6 +136,30 @@ impl IndexUrl { Cow::Owned(url) } } + + /// Warn user if the given URL was provided as an ambiguous relative path. + /// + /// This is a temporary warning. Ambiguous values will not be + /// accepted in the future. + pub fn warn_on_disambiguated_relative_path(&self) { + let Self::Path(verbatim_url) = &self else { + return; + }; + + if let Some(path) = verbatim_url.given() { + if !is_disambiguated_path(path) { + if cfg!(windows) { + warn_user!( + "Relative paths passed to `--index` or `--default-index` should be disambiguated from index names (use `.\\{path}` or `./{path}`). Support for ambiguous values will be removed in the future" + ); + } else { + warn_user!( + "Relative paths passed to `--index` or `--default-index` should be disambiguated from index names (use `./{path}`). Support for ambiguous values will be removed in the future" + ); + } + } + } + } } impl Display for IndexUrl { @@ -157,6 +182,28 @@ impl Verbatim for IndexUrl { } } +/// Checks if a path is disambiguated. +/// +/// Disambiguated paths are absolute paths, paths with valid schemes, +/// and paths starting with "./" or "../" on Unix or ".\\", "..\\", +/// "./", or "../" on Windows. +fn is_disambiguated_path(path: &str) -> bool { + if cfg!(windows) { + if path.starts_with(".\\") || path.starts_with("..\\") || path.starts_with('/') { + return true; + } + } + if path.starts_with("./") || path.starts_with("../") || Path::new(path).is_absolute() { + return true; + } + // Check if the path has a scheme (like `file://`) + if let Some((scheme, _)) = split_scheme(path) { + return Scheme::parse(scheme).is_some(); + } + // This is an ambiguous relative path + false +} + /// An error that can occur when parsing an [`IndexUrl`]. #[derive(Error, Debug)] pub enum IndexUrlError { @@ -620,3 +667,41 @@ impl IndexCapabilities { .insert(Flags::FORBIDDEN); } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_index_url_parse_valid_paths() { + // Absolute path + assert!(is_disambiguated_path("/absolute/path")); + // Relative path + assert!(is_disambiguated_path("./relative/path")); + assert!(is_disambiguated_path("../../relative/path")); + if cfg!(windows) { + // Windows absolute path + assert!(is_disambiguated_path("C:/absolute/path")); + // Windows relative path + assert!(is_disambiguated_path(".\\relative\\path")); + assert!(is_disambiguated_path("..\\..\\relative\\path")); + } + } + + #[test] + fn test_index_url_parse_ambiguous_paths() { + // Test single-segment ambiguous path + assert!(!is_disambiguated_path("index")); + // Test multi-segment ambiguous path + assert!(!is_disambiguated_path("relative/path")); + } + + #[test] + fn test_index_url_parse_with_schemes() { + assert!(is_disambiguated_path("file:///absolute/path")); + assert!(is_disambiguated_path("https://registry.com/simple/")); + assert!(is_disambiguated_path( + "git+https://github.com/example/repo.git" + )); + } +} diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 5cbeb1886..58a012d89 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -1386,6 +1386,12 @@ impl AddSettings { ) .collect::>(); + // Warn user if an ambiguous relative path was passed as a value for + // `--index` or `--default-index`. + indexes + .iter() + .for_each(|index| index.url().warn_on_disambiguated_relative_path()); + // If the user passed an `--index-url` or `--extra-index-url`, warn. if installer .index_args diff --git a/crates/uv/tests/it/edit.rs b/crates/uv/tests/it/edit.rs index 6bdaec17b..2f9c91e84 100644 --- a/crates/uv/tests/it/edit.rs +++ b/crates/uv/tests/it/edit.rs @@ -9446,7 +9446,7 @@ fn add_index_with_existing_relative_path_index() -> Result<()> { let wheel_dst = packages.child("ok-1.0.0-py3-none-any.whl"); fs_err::copy(&wheel_src, &wheel_dst)?; - uv_snapshot!(context.filters(), context.add().arg("iniconfig").arg("--index").arg("test-index"), @r" + uv_snapshot!(context.filters(), context.add().arg("iniconfig").arg("--index").arg("./test-index"), @r" success: true exit_code: 0 ----- stdout ----- @@ -9475,7 +9475,7 @@ fn add_index_with_non_existent_relative_path() -> Result<()> { dependencies = [] "#})?; - uv_snapshot!(context.filters(), context.add().arg("iniconfig").arg("--index").arg("test-index"), @r" + uv_snapshot!(context.filters(), context.add().arg("iniconfig").arg("--index").arg("./test-index"), @r" success: false exit_code: 2 ----- stdout ----- @@ -9505,7 +9505,7 @@ fn add_index_with_non_existent_relative_path_with_same_name_as_index() -> Result url = "https://pypi-proxy.fly.dev/simple" "#})?; - uv_snapshot!(context.filters(), context.add().arg("iniconfig").arg("--index").arg("test-index"), @r" + uv_snapshot!(context.filters(), context.add().arg("iniconfig").arg("--index").arg("./test-index"), @r" success: false exit_code: 2 ----- stdout ----- @@ -9528,12 +9528,16 @@ fn add_index_empty_directory() -> Result<()> { version = "0.1.0" requires-python = ">=3.12" dependencies = [] + + [[tool.uv.index]] + name = "test-index" + url = "https://pypi-proxy.fly.dev/simple" "#})?; let packages = context.temp_dir.child("test-index"); packages.create_dir_all()?; - uv_snapshot!(context.filters(), context.add().arg("iniconfig").arg("--index").arg("test-index"), @r" + uv_snapshot!(context.filters(), context.add().arg("iniconfig").arg("--index").arg("./test-index"), @r" success: true exit_code: 0 ----- stdout ----- @@ -9549,6 +9553,46 @@ fn add_index_empty_directory() -> Result<()> { Ok(()) } +#[test] +fn add_index_with_ambiguous_relative_path() -> Result<()> { + let context = TestContext::new("3.12"); + let mut filters = context.filters(); + filters.push((r"\./|\.\\\\", r"[PREFIX]")); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! {r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + "#})?; + + #[cfg(unix)] + uv_snapshot!(filters, context.add().arg("iniconfig").arg("--index").arg("test-index"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + warning: Relative paths passed to `--index` or `--default-index` should be disambiguated from index names (use `[PREFIX]test-index`). Support for ambiguous values will be removed in the future + error: Directory not found for index: file://[TEMP_DIR]/test-index + "); + + #[cfg(windows)] + uv_snapshot!(filters, context.add().arg("iniconfig").arg("--index").arg("test-index"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + warning: Relative paths passed to `--index` or `--default-index` should be disambiguated from index names (use `[PREFIX]test-index` or `[PREFIX]test-index`). Support for ambiguous values will be removed in the future + error: Directory not found for index: file://[TEMP_DIR]/test-index + "); + + Ok(()) +} + /// Add a PyPI requirement. #[test] fn add_group_comment() -> Result<()> { diff --git a/docs/reference/cli.md b/docs/reference/cli.md index b9490e2ce..82fe0fa3d 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -123,6 +123,7 @@ uv run [OPTIONS] [COMMAND]
    --index index

    The URLs to use when resolving dependencies, in addition to the default index.

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    All indexes provided via this flag take priority over the index specified by --default-index (which defaults to PyPI). When multiple --index flags are provided, earlier values take priority.

    +

    Index names are not supported as values. Relative paths must be disambiguated from index names with ./ or ../ on Unix or .\\, ..\\, ./ or ../ on Windows.

    May also be set with the UV_INDEX environment variable.

    --index-strategy index-strategy

    The strategy to use when resolving against multiple index URLs.

    By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-index). This prevents "dependency confusion" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.

    May also be set with the UV_INDEX_STRATEGY environment variable.

    Possible values:

    @@ -479,6 +480,7 @@ uv add [OPTIONS] >
    --index index

    The URLs to use when resolving dependencies, in addition to the default index.

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    All indexes provided via this flag take priority over the index specified by --default-index (which defaults to PyPI). When multiple --index flags are provided, earlier values take priority.

    +

    Index names are not supported as values. Relative paths must be disambiguated from index names with ./ or ../ on Unix or .\\, ..\\, ./ or ../ on Windows.

    May also be set with the UV_INDEX environment variable.

    --index-strategy index-strategy

    The strategy to use when resolving against multiple index URLs.

    By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-index). This prevents "dependency confusion" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.

    May also be set with the UV_INDEX_STRATEGY environment variable.

    Possible values:

    @@ -663,6 +665,7 @@ uv remove [OPTIONS] ...
    --index index

    The URLs to use when resolving dependencies, in addition to the default index.

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    All indexes provided via this flag take priority over the index specified by --default-index (which defaults to PyPI). When multiple --index flags are provided, earlier values take priority.

    +

    Index names are not supported as values. Relative paths must be disambiguated from index names with ./ or ../ on Unix or .\\, ..\\, ./ or ../ on Windows.

    May also be set with the UV_INDEX environment variable.

    --index-strategy index-strategy

    The strategy to use when resolving against multiple index URLs.

    By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-index). This prevents "dependency confusion" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.

    May also be set with the UV_INDEX_STRATEGY environment variable.

    Possible values:

    @@ -832,6 +835,7 @@ uv version [OPTIONS] [VALUE]
    --index index

    The URLs to use when resolving dependencies, in addition to the default index.

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    All indexes provided via this flag take priority over the index specified by --default-index (which defaults to PyPI). When multiple --index flags are provided, earlier values take priority.

    +

    Index names are not supported as values. Relative paths must be disambiguated from index names with ./ or ../ on Unix or .\\, ..\\, ./ or ../ on Windows.

    May also be set with the UV_INDEX environment variable.

    --index-strategy index-strategy

    The strategy to use when resolving against multiple index URLs.

    By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-index). This prevents "dependency confusion" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.

    May also be set with the UV_INDEX_STRATEGY environment variable.

    Possible values:

    @@ -1022,6 +1026,7 @@ uv sync [OPTIONS]
    --index index

    The URLs to use when resolving dependencies, in addition to the default index.

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    All indexes provided via this flag take priority over the index specified by --default-index (which defaults to PyPI). When multiple --index flags are provided, earlier values take priority.

    +

    Index names are not supported as values. Relative paths must be disambiguated from index names with ./ or ../ on Unix or .\\, ..\\, ./ or ../ on Windows.

    May also be set with the UV_INDEX environment variable.

    --index-strategy index-strategy

    The strategy to use when resolving against multiple index URLs.

    By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-index). This prevents "dependency confusion" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.

    May also be set with the UV_INDEX_STRATEGY environment variable.

    Possible values:

    @@ -1210,6 +1215,7 @@ uv lock [OPTIONS]
    --index index

    The URLs to use when resolving dependencies, in addition to the default index.

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    All indexes provided via this flag take priority over the index specified by --default-index (which defaults to PyPI). When multiple --index flags are provided, earlier values take priority.

    +

    Index names are not supported as values. Relative paths must be disambiguated from index names with ./ or ../ on Unix or .\\, ..\\, ./ or ../ on Windows.

    May also be set with the UV_INDEX environment variable.

    --index-strategy index-strategy

    The strategy to use when resolving against multiple index URLs.

    By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-index). This prevents "dependency confusion" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.

    May also be set with the UV_INDEX_STRATEGY environment variable.

    Possible values:

    @@ -1383,6 +1389,7 @@ uv export [OPTIONS]
    --index index

    The URLs to use when resolving dependencies, in addition to the default index.

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    All indexes provided via this flag take priority over the index specified by --default-index (which defaults to PyPI). When multiple --index flags are provided, earlier values take priority.

    +

    Index names are not supported as values. Relative paths must be disambiguated from index names with ./ or ../ on Unix or .\\, ..\\, ./ or ../ on Windows.

    May also be set with the UV_INDEX environment variable.

    --index-strategy index-strategy

    The strategy to use when resolving against multiple index URLs.

    By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-index). This prevents "dependency confusion" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.

    May also be set with the UV_INDEX_STRATEGY environment variable.

    Possible values:

    @@ -1568,6 +1575,7 @@ uv tree [OPTIONS]
    --index index

    The URLs to use when resolving dependencies, in addition to the default index.

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    All indexes provided via this flag take priority over the index specified by --default-index (which defaults to PyPI). When multiple --index flags are provided, earlier values take priority.

    +

    Index names are not supported as values. Relative paths must be disambiguated from index names with ./ or ../ on Unix or .\\, ..\\, ./ or ../ on Windows.

    May also be set with the UV_INDEX environment variable.

    --index-strategy index-strategy

    The strategy to use when resolving against multiple index URLs.

    By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-index). This prevents "dependency confusion" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.

    May also be set with the UV_INDEX_STRATEGY environment variable.

    Possible values:

    @@ -1827,6 +1835,7 @@ uv tool run [OPTIONS] [COMMAND]
    --index index

    The URLs to use when resolving dependencies, in addition to the default index.

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    All indexes provided via this flag take priority over the index specified by --default-index (which defaults to PyPI). When multiple --index flags are provided, earlier values take priority.

    +

    Index names are not supported as values. Relative paths must be disambiguated from index names with ./ or ../ on Unix or .\\, ..\\, ./ or ../ on Windows.

    May also be set with the UV_INDEX environment variable.

    --index-strategy index-strategy

    The strategy to use when resolving against multiple index URLs.

    By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-index). This prevents "dependency confusion" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.

    May also be set with the UV_INDEX_STRATEGY environment variable.

    Possible values:

    @@ -1997,6 +2006,7 @@ uv tool install [OPTIONS]
    --index index

    The URLs to use when resolving dependencies, in addition to the default index.

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    All indexes provided via this flag take priority over the index specified by --default-index (which defaults to PyPI). When multiple --index flags are provided, earlier values take priority.

    +

    Index names are not supported as values. Relative paths must be disambiguated from index names with ./ or ../ on Unix or .\\, ..\\, ./ or ../ on Windows.

    May also be set with the UV_INDEX environment variable.

    --index-strategy index-strategy

    The strategy to use when resolving against multiple index URLs.

    By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-index). This prevents "dependency confusion" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.

    May also be set with the UV_INDEX_STRATEGY environment variable.

    Possible values:

    @@ -2157,6 +2167,7 @@ uv tool upgrade [OPTIONS] ...
    --index index

    The URLs to use when resolving dependencies, in addition to the default index.

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    All indexes provided via this flag take priority over the index specified by --default-index (which defaults to PyPI). When multiple --index flags are provided, earlier values take priority.

    +

    Index names are not supported as values. Relative paths must be disambiguated from index names with ./ or ../ on Unix or .\\, ..\\, ./ or ../ on Windows.

    May also be set with the UV_INDEX environment variable.

    --index-strategy index-strategy

    The strategy to use when resolving against multiple index URLs.

    By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-index). This prevents "dependency confusion" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.

    May also be set with the UV_INDEX_STRATEGY environment variable.

    Possible values:

    @@ -3252,6 +3263,7 @@ uv pip compile [OPTIONS] >
    --index index

    The URLs to use when resolving dependencies, in addition to the default index.

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    All indexes provided via this flag take priority over the index specified by --default-index (which defaults to PyPI). When multiple --index flags are provided, earlier values take priority.

    +

    Index names are not supported as values. Relative paths must be disambiguated from index names with ./ or ../ on Unix or .\\, ..\\, ./ or ../ on Windows.

    May also be set with the UV_INDEX environment variable.

    --index-strategy index-strategy

    The strategy to use when resolving against multiple index URLs.

    By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-index). This prevents "dependency confusion" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.

    May also be set with the UV_INDEX_STRATEGY environment variable.

    Possible values:

    @@ -3531,6 +3543,7 @@ uv pip sync [OPTIONS] ...
    --index index

    The URLs to use when resolving dependencies, in addition to the default index.

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    All indexes provided via this flag take priority over the index specified by --default-index (which defaults to PyPI). When multiple --index flags are provided, earlier values take priority.

    +

    Index names are not supported as values. Relative paths must be disambiguated from index names with ./ or ../ on Unix or .\\, ..\\, ./ or ../ on Windows.

    May also be set with the UV_INDEX environment variable.

    --index-strategy index-strategy

    The strategy to use when resolving against multiple index URLs.

    By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-index). This prevents "dependency confusion" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.

    May also be set with the UV_INDEX_STRATEGY environment variable.

    Possible values:

    @@ -3795,6 +3808,7 @@ uv pip install [OPTIONS] |--editable
    --index index

    The URLs to use when resolving dependencies, in addition to the default index.

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    All indexes provided via this flag take priority over the index specified by --default-index (which defaults to PyPI). When multiple --index flags are provided, earlier values take priority.

    +

    Index names are not supported as values. Relative paths must be disambiguated from index names with ./ or ../ on Unix or .\\, ..\\, ./ or ../ on Windows.

    May also be set with the UV_INDEX environment variable.

    --index-strategy index-strategy

    The strategy to use when resolving against multiple index URLs.

    By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-index). This prevents "dependency confusion" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.

    May also be set with the UV_INDEX_STRATEGY environment variable.

    Possible values:

    @@ -4214,6 +4228,7 @@ uv pip list [OPTIONS]
    --index index

    The URLs to use when resolving dependencies, in addition to the default index.

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    All indexes provided via this flag take priority over the index specified by --default-index (which defaults to PyPI). When multiple --index flags are provided, earlier values take priority.

    +

    Index names are not supported as values. Relative paths must be disambiguated from index names with ./ or ../ on Unix or .\\, ..\\, ./ or ../ on Windows.

    May also be set with the UV_INDEX environment variable.

    --index-strategy index-strategy

    The strategy to use when resolving against multiple index URLs.

    By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-index). This prevents "dependency confusion" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.

    May also be set with the UV_INDEX_STRATEGY environment variable.

    Possible values:

    @@ -4387,6 +4402,7 @@ uv pip tree [OPTIONS]
    --index index

    The URLs to use when resolving dependencies, in addition to the default index.

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    All indexes provided via this flag take priority over the index specified by --default-index (which defaults to PyPI). When multiple --index flags are provided, earlier values take priority.

    +

    Index names are not supported as values. Relative paths must be disambiguated from index names with ./ or ../ on Unix or .\\, ..\\, ./ or ../ on Windows.

    May also be set with the UV_INDEX environment variable.

    --index-strategy index-strategy

    The strategy to use when resolving against multiple index URLs.

    By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-index). This prevents "dependency confusion" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.

    May also be set with the UV_INDEX_STRATEGY environment variable.

    Possible values:

    @@ -4574,6 +4590,7 @@ uv venv [OPTIONS] [PATH]
    --index index

    The URLs to use when resolving dependencies, in addition to the default index.

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    All indexes provided via this flag take priority over the index specified by --default-index (which defaults to PyPI). When multiple --index flags are provided, earlier values take priority.

    +

    Index names are not supported as values. Relative paths must be disambiguated from index names with ./ or ../ on Unix or .\\, ..\\, ./ or ../ on Windows.

    May also be set with the UV_INDEX environment variable.

    --index-strategy index-strategy

    The strategy to use when resolving against multiple index URLs.

    By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-index). This prevents "dependency confusion" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.

    May also be set with the UV_INDEX_STRATEGY environment variable.

    Possible values:

    @@ -4724,6 +4741,7 @@ uv build [OPTIONS] [SRC]
    --index index

    The URLs to use when resolving dependencies, in addition to the default index.

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    All indexes provided via this flag take priority over the index specified by --default-index (which defaults to PyPI). When multiple --index flags are provided, earlier values take priority.

    +

    Index names are not supported as values. Relative paths must be disambiguated from index names with ./ or ../ on Unix or .\\, ..\\, ./ or ../ on Windows.

    May also be set with the UV_INDEX environment variable.

    --index-strategy index-strategy

    The strategy to use when resolving against multiple index URLs.

    By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-index). This prevents "dependency confusion" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.

    May also be set with the UV_INDEX_STRATEGY environment variable.

    Possible values:

    From 177df19f303b6021754cd8629d7369c0bca9d76c Mon Sep 17 00:00:00 2001 From: John Mumm Date: Wed, 25 Jun 2025 04:12:32 -0400 Subject: [PATCH 086/185] Add check for using minor version link when creating a venv on Windows (#14252) There was a regression introduced in #13954 on Windows where creating a venv behaved as if there was a minor version link even if none existed. This PR adds a check to fix this. Closes #14249. --- crates/uv-virtualenv/src/virtualenv.rs | 4 ++- crates/uv/tests/it/venv.rs | 43 +++++++++++++++++++++++--- 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/crates/uv-virtualenv/src/virtualenv.rs b/crates/uv-virtualenv/src/virtualenv.rs index f466233c0..bad380c4c 100644 --- a/crates/uv-virtualenv/src/virtualenv.rs +++ b/crates/uv-virtualenv/src/virtualenv.rs @@ -147,6 +147,7 @@ pub(crate) fn create( // Create a `.gitignore` file to ignore all files in the venv. fs::write(location.join(".gitignore"), "*")?; + let mut using_minor_version_link = false; let executable_target = if upgradeable && interpreter.is_standalone() { if let Some(minor_version_link) = PythonMinorVersionLink::from_executable( base_python.as_path(), @@ -167,6 +168,7 @@ pub(crate) fn create( &minor_version_link.symlink_directory.display(), &base_python.display() ); + using_minor_version_link = true; minor_version_link.symlink_executable.clone() } } else { @@ -228,7 +230,7 @@ pub(crate) fn create( // interpreters, this target path includes a minor version junction to enable // transparent upgrades. if cfg!(windows) { - if interpreter.is_standalone() { + if using_minor_version_link { let target = scripts.join(WindowsExecutable::Python.exe(interpreter)); create_link_to_executable(target.as_path(), executable_target.clone()) .map_err(Error::Python)?; diff --git a/crates/uv/tests/it/venv.rs b/crates/uv/tests/it/venv.rs index f1860efa2..52291c05d 100644 --- a/crates/uv/tests/it/venv.rs +++ b/crates/uv/tests/it/venv.rs @@ -516,7 +516,7 @@ fn create_venv_respects_group_requires_python() -> Result<()> { name = "foo" version = "1.0.0" dependencies = [] - + [dependency-groups] dev = ["sortedcontainers"] other = ["sniffio"] @@ -549,7 +549,7 @@ fn create_venv_respects_group_requires_python() -> Result<()> { version = "1.0.0" requires-python = ">=3.11" dependencies = [] - + [dependency-groups] dev = ["sortedcontainers"] other = ["sniffio"] @@ -582,7 +582,7 @@ fn create_venv_respects_group_requires_python() -> Result<()> { version = "1.0.0" requires-python = ">=3.10" dependencies = [] - + [dependency-groups] dev = ["sortedcontainers"] other = ["sniffio"] @@ -612,7 +612,7 @@ fn create_venv_respects_group_requires_python() -> Result<()> { name = "foo" version = "1.0.0" dependencies = [] - + [dependency-groups] dev = ["sortedcontainers"] @@ -643,7 +643,7 @@ fn create_venv_respects_group_requires_python() -> Result<()> { version = "1.0.0" requires-python = "<3.12" dependencies = [] - + [dependency-groups] dev = ["sortedcontainers"] other = ["sniffio"] @@ -1052,6 +1052,39 @@ fn non_empty_dir_exists_allow_existing() -> Result<()> { Ok(()) } +/// Run `uv venv` followed by `uv venv --allow-existing`. +#[test] +fn create_venv_then_allow_existing() { + let context = TestContext::new_with_versions(&["3.12"]); + + // Create a venv + uv_snapshot!(context.filters(), context.venv(), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] + Creating virtual environment at: .venv + Activate with: source .venv/[BIN]/activate + " + ); + + // Create a venv again with `--allow-existing` + uv_snapshot!(context.filters(), context.venv() + .arg("--allow-existing"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] + Creating virtual environment at: .venv + Activate with: source .venv/[BIN]/activate + "### + ); +} + #[test] #[cfg(windows)] fn windows_shims() -> Result<()> { From 4ed9c5791ba9d5c304aae14b920d612af26dc2d3 Mon Sep 17 00:00:00 2001 From: John Mumm Date: Wed, 25 Jun 2025 06:06:41 -0400 Subject: [PATCH 087/185] Bump version to 0.7.15 (#14254) --- CHANGELOG.md | 68 ++++++++++++++++++--------- Cargo.lock | 6 +-- crates/uv-build/Cargo.toml | 2 +- crates/uv-build/pyproject.toml | 2 +- crates/uv-version/Cargo.toml | 2 +- crates/uv/Cargo.toml | 2 +- docs/concepts/build-backend.md | 2 +- docs/getting-started/installation.md | 4 +- docs/guides/integration/aws-lambda.md | 4 +- docs/guides/integration/docker.md | 10 ++-- docs/guides/integration/github.md | 2 +- docs/guides/integration/pre-commit.md | 10 ++-- pyproject.toml | 2 +- 13 files changed, 69 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 159c39331..7ef35e9a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,28 @@ +## 0.7.15 + +### Enhancements + +- Consistently use `Ordering::Relaxed` for standalone atomic use cases ([#14190](https://github.com/astral-sh/uv/pull/14190)) +- Warn on ambiguous relative paths for `--index` ([#14152](https://github.com/astral-sh/uv/pull/14152)) +- Skip GitHub fast path when rate-limited ([#13033](https://github.com/astral-sh/uv/pull/13033)) +- Preserve newlines in `schema.json` descriptions ([#13693](https://github.com/astral-sh/uv/pull/13693)) + +### Bug fixes + +- Add check for using minor version link when creating a venv on Windows ([#14252](https://github.com/astral-sh/uv/pull/14252)) +- Strip query parameters when parsing source URL ([#14224](https://github.com/astral-sh/uv/pull/14224)) + +### Documentation + +- Add a link to PyPI FAQ to clarify what per-project token is ([#14242](https://github.com/astral-sh/uv/pull/14242)) + +### Preview features + +- Allow symlinks in the build backend ([#14212](https://github.com/astral-sh/uv/pull/14212)) + ## 0.7.14 ### Enhancements @@ -386,11 +408,11 @@ This release contains various changes that improve correctness and user experien ### Breaking changes - **Update `uv version` to display and update project versions ([#12349](https://github.com/astral-sh/uv/pull/12349))** - + Previously, `uv version` displayed uv's version. Now, `uv version` will display or update the project's version. This interface was [heavily requested](https://github.com/astral-sh/uv/issues/6298) and, after much consideration, we decided that transitioning the top-level command was the best option. - + Here's a brief example: - + ```console $ uv init example Initialized project `example` at `./example` @@ -402,72 +424,72 @@ This release contains various changes that improve correctness and user experien $ uv version --short 1.0.0 ``` - + If used outside of a project, uv will fallback to showing its own version still: - + ```console $ uv version warning: failed to read project: No `pyproject.toml` found in current directory or any parent directory running `uv self version` for compatibility with old `uv version` command. this fallback will be removed soon, pass `--preview` to make this an error. - + uv 0.7.0 (4433f41c9 2025-04-29) ``` - + As described in the warning, `--preview` can be used to error instead: - + ```console $ uv version --preview error: No `pyproject.toml` found in current directory or any parent directory ``` - + The previous functionality of `uv version` was moved to `uv self version`. - **Avoid fallback to subsequent indexes on authentication failure ([#12805](https://github.com/astral-sh/uv/pull/12805))** - + When using the `first-index` strategy (the default), uv will stop searching indexes for a package once it is found on a single index. Previously, uv considered a package as "missing" from an index during authentication failures, such as an HTTP 401 or HTTP 403 (normally, missing packages are represented by an HTTP 404). This behavior was motivated by unusual responses from some package indexes, but reduces the safety of uv's index strategy when authentication fails. Now, uv will consider an authentication failure as a stop-point when searching for a package across indexes. The `index.ignore-error-codes` option can be used to recover the existing behavior, e.g.: - + ```toml [[tool.uv.index]] name = "pytorch" url = "https://download.pytorch.org/whl/cpu" ignore-error-codes = [401, 403] ``` - + Since PyTorch's indexes always return a HTTP 403 for missing packages, uv special-cases indexes on the `pytorch.org` domain to ignore that error code by default. - **Require the command in `uvx ` to be available in the Python environment ([#11603](https://github.com/astral-sh/uv/pull/11603))** - + Previously, `uvx` would attempt to execute a command even if it was not provided by a Python package. For example, if we presume `foo` is an empty Python package which provides no command, `uvx foo` would invoke the `foo` command on the `PATH` (if present). Now, uv will error early if the `foo` executable is not provided by the requested Python package. This check is not enforced when `--from` is used, so patterns like `uvx --from foo bash -c "..."` are still valid. uv also still allows `uvx foo` where the `foo` executable is provided by a dependency of `foo` instead of `foo` itself, as this is fairly common for packages which depend on a dedicated package for their command-line interface. - **Use index URL instead of package URL for keyring credential lookups ([#12651](https://github.com/astral-sh/uv/pull/12651))** - + When determining credentials for querying a package URL, uv previously sent the full URL to the `keyring` command. However, some keyring plugins expect to receive the *index URL* (which is usually a parent of the package URL). Now, uv requests credentials for the index URL instead. This behavior matches `pip`. - **Remove `--version` from subcommands ([#13108](https://github.com/astral-sh/uv/pull/13108))** - + Previously, uv allowed the `--version` flag on arbitrary subcommands, e.g., `uv run --version`. However, the `--version` flag is useful for other operations since uv is a package manager. Consequently, we've removed the `--version` flag from subcommands — it is only available as `uv --version`. - **Omit Python 3.7 downloads from managed versions ([#13022](https://github.com/astral-sh/uv/pull/13022))** - + Python 3.7 is EOL and not formally supported by uv; however, Python 3.7 was previously available for download on a subset of platforms. - **Reject non-PEP 751 TOML files in install, compile, and export commands ([#13120](https://github.com/astral-sh/uv/pull/13120), [#13119](https://github.com/astral-sh/uv/pull/13119))** - + Previously, uv treated arbitrary `.toml` files passed to commands (e.g., `uv pip install -r foo.toml` or `uv pip compile -o foo.toml`) as `requirements.txt`-formatted files. Now, uv will error instead. If using PEP 751 lockfiles, use the standardized format for custom names instead, e.g., `pylock.foo.toml`. - **Ignore arbitrary Python requests in version files ([#12909](https://github.com/astral-sh/uv/pull/12909))** - + uv allows arbitrary strings to be used for Python version requests, in which they are treated as an executable name to search for in the `PATH`. However, using this form of request in `.python-version` files is non-standard and conflicts with `pyenv-virtualenv` which writes environment names to `.python-version` files. In this release, uv will now ignore requests that are arbitrary strings when found in `.python-version` files. - **Error on unknown dependency object specifiers ([12811](https://github.com/astral-sh/uv/pull/12811))** - + The `[dependency-groups]` entries can include "object specifiers", e.g. `set-phasers-to = ...` in: - + ```toml [dependency-groups] foo = ["pyparsing"] bar = [{set-phasers-to = "stun"}] ``` - + However, the only current spec-compliant object specifier is `include-group`. Previously, uv would ignore unknown object specifiers. Now, uv will error. - **Make `--frozen` and `--no-sources` conflicting options ([#12671](https://github.com/astral-sh/uv/pull/12671))** - + Using `--no-sources` always requires a new resolution and `--frozen` will always fail when used with it. Now, this conflict is encoded in the CLI options for clarity. - **Treat empty `UV_PYTHON_INSTALL_DIR` and `UV_TOOL_DIR` as unset ([#12907](https://github.com/astral-sh/uv/pull/12907), [#12905](https://github.com/astral-sh/uv/pull/12905))** - + Previously, these variables were treated as set to the current working directory when set to an empty string. Now, uv will ignore these variables when empty. This matches uv's behavior for other environment variables which configure directories. ### Enhancements diff --git a/Cargo.lock b/Cargo.lock index 781f98079..75b6fe7e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4570,7 +4570,7 @@ dependencies = [ [[package]] name = "uv" -version = "0.7.14" +version = "0.7.15" dependencies = [ "anstream", "anyhow", @@ -4735,7 +4735,7 @@ dependencies = [ [[package]] name = "uv-build" -version = "0.7.14" +version = "0.7.15" dependencies = [ "anyhow", "uv-build-backend", @@ -5923,7 +5923,7 @@ dependencies = [ [[package]] name = "uv-version" -version = "0.7.14" +version = "0.7.15" [[package]] name = "uv-virtualenv" diff --git a/crates/uv-build/Cargo.toml b/crates/uv-build/Cargo.toml index 26c046b2b..94b4adbc3 100644 --- a/crates/uv-build/Cargo.toml +++ b/crates/uv-build/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-build" -version = "0.7.14" +version = "0.7.15" edition.workspace = true rust-version.workspace = true homepage.workspace = true diff --git a/crates/uv-build/pyproject.toml b/crates/uv-build/pyproject.toml index 4dab6a520..4b8c522d6 100644 --- a/crates/uv-build/pyproject.toml +++ b/crates/uv-build/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uv-build" -version = "0.7.14" +version = "0.7.15" description = "The uv build backend" authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }] requires-python = ">=3.8" diff --git a/crates/uv-version/Cargo.toml b/crates/uv-version/Cargo.toml index b92014be4..9eef680a2 100644 --- a/crates/uv-version/Cargo.toml +++ b/crates/uv-version/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-version" -version = "0.7.14" +version = "0.7.15" edition = { workspace = true } rust-version = { workspace = true } homepage = { workspace = true } diff --git a/crates/uv/Cargo.toml b/crates/uv/Cargo.toml index e63cbfe40..f4afb87fa 100644 --- a/crates/uv/Cargo.toml +++ b/crates/uv/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv" -version = "0.7.14" +version = "0.7.15" edition = { workspace = true } rust-version = { workspace = true } homepage = { workspace = true } diff --git a/docs/concepts/build-backend.md b/docs/concepts/build-backend.md index 2162aaaa5..2e93cc546 100644 --- a/docs/concepts/build-backend.md +++ b/docs/concepts/build-backend.md @@ -19,7 +19,7 @@ existing project, add it to the `[build-system]` section in your `pyproject.toml ```toml [build-system] -requires = ["uv_build>=0.7.14,<0.8.0"] +requires = ["uv_build>=0.7.15,<0.8.0"] build-backend = "uv_build" ``` diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index a87ce695a..33281a0c8 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -25,7 +25,7 @@ uv provides a standalone installer to download and install uv: Request a specific version by including it in the URL: ```console - $ curl -LsSf https://astral.sh/uv/0.7.14/install.sh | sh + $ curl -LsSf https://astral.sh/uv/0.7.15/install.sh | sh ``` === "Windows" @@ -41,7 +41,7 @@ uv provides a standalone installer to download and install uv: Request a specific version by including it in the URL: ```pwsh-session - PS> powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/0.7.14/install.ps1 | iex" + PS> powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/0.7.15/install.ps1 | iex" ``` !!! tip diff --git a/docs/guides/integration/aws-lambda.md b/docs/guides/integration/aws-lambda.md index 467088736..b48dba1b1 100644 --- a/docs/guides/integration/aws-lambda.md +++ b/docs/guides/integration/aws-lambda.md @@ -92,7 +92,7 @@ the second stage, we'll copy this directory over to the final image, omitting th other unnecessary files. ```dockerfile title="Dockerfile" -FROM ghcr.io/astral-sh/uv:0.7.14 AS uv +FROM ghcr.io/astral-sh/uv:0.7.15 AS uv # First, bundle the dependencies into the task root. FROM public.ecr.aws/lambda/python:3.13 AS builder @@ -334,7 +334,7 @@ And confirm that opening http://127.0.0.1:8000/ in a web browser displays, "Hell Finally, we'll update the Dockerfile to include the local library in the deployment package: ```dockerfile title="Dockerfile" -FROM ghcr.io/astral-sh/uv:0.7.14 AS uv +FROM ghcr.io/astral-sh/uv:0.7.15 AS uv # First, bundle the dependencies into the task root. FROM public.ecr.aws/lambda/python:3.13 AS builder diff --git a/docs/guides/integration/docker.md b/docs/guides/integration/docker.md index 01e66f775..d8e6d69a3 100644 --- a/docs/guides/integration/docker.md +++ b/docs/guides/integration/docker.md @@ -31,7 +31,7 @@ $ docker run --rm -it ghcr.io/astral-sh/uv:debian uv --help The following distroless images are available: - `ghcr.io/astral-sh/uv:latest` -- `ghcr.io/astral-sh/uv:{major}.{minor}.{patch}`, e.g., `ghcr.io/astral-sh/uv:0.7.14` +- `ghcr.io/astral-sh/uv:{major}.{minor}.{patch}`, e.g., `ghcr.io/astral-sh/uv:0.7.15` - `ghcr.io/astral-sh/uv:{major}.{minor}`, e.g., `ghcr.io/astral-sh/uv:0.7` (the latest patch version) @@ -75,7 +75,7 @@ And the following derived images are available: As with the distroless image, each derived image is published with uv version tags as `ghcr.io/astral-sh/uv:{major}.{minor}.{patch}-{base}` and -`ghcr.io/astral-sh/uv:{major}.{minor}-{base}`, e.g., `ghcr.io/astral-sh/uv:0.7.14-alpine`. +`ghcr.io/astral-sh/uv:{major}.{minor}-{base}`, e.g., `ghcr.io/astral-sh/uv:0.7.15-alpine`. For more details, see the [GitHub Container](https://github.com/astral-sh/uv/pkgs/container/uv) page. @@ -113,7 +113,7 @@ Note this requires `curl` to be available. In either case, it is best practice to pin to a specific uv version, e.g., with: ```dockerfile -COPY --from=ghcr.io/astral-sh/uv:0.7.14 /uv /uvx /bin/ +COPY --from=ghcr.io/astral-sh/uv:0.7.15 /uv /uvx /bin/ ``` !!! tip @@ -131,7 +131,7 @@ COPY --from=ghcr.io/astral-sh/uv:0.7.14 /uv /uvx /bin/ Or, with the installer: ```dockerfile -ADD https://astral.sh/uv/0.7.14/install.sh /uv-installer.sh +ADD https://astral.sh/uv/0.7.15/install.sh /uv-installer.sh ``` ### Installing a project @@ -557,5 +557,5 @@ Verified OK !!! tip These examples use `latest`, but best practice is to verify the attestation for a specific - version tag, e.g., `ghcr.io/astral-sh/uv:0.7.14`, or (even better) the specific image digest, + version tag, e.g., `ghcr.io/astral-sh/uv:0.7.15`, or (even better) the specific image digest, such as `ghcr.io/astral-sh/uv:0.5.27@sha256:5adf09a5a526f380237408032a9308000d14d5947eafa687ad6c6a2476787b4f`. diff --git a/docs/guides/integration/github.md b/docs/guides/integration/github.md index 0bd1cc1b1..ea96199a2 100644 --- a/docs/guides/integration/github.md +++ b/docs/guides/integration/github.md @@ -47,7 +47,7 @@ jobs: uses: astral-sh/setup-uv@v5 with: # Install a specific version of uv. - version: "0.7.14" + version: "0.7.15" ``` ## Setting up Python diff --git a/docs/guides/integration/pre-commit.md b/docs/guides/integration/pre-commit.md index 326832e7e..9e78d1595 100644 --- a/docs/guides/integration/pre-commit.md +++ b/docs/guides/integration/pre-commit.md @@ -19,7 +19,7 @@ To make sure your `uv.lock` file is up to date even if your `pyproject.toml` fil repos: - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.7.14 + rev: 0.7.15 hooks: - id: uv-lock ``` @@ -30,7 +30,7 @@ To keep a `requirements.txt` file in sync with your `uv.lock` file: repos: - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.7.14 + rev: 0.7.15 hooks: - id: uv-export ``` @@ -41,7 +41,7 @@ To compile requirements files: repos: - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.7.14 + rev: 0.7.15 hooks: # Compile requirements - id: pip-compile @@ -54,7 +54,7 @@ To compile alternative requirements files, modify `args` and `files`: repos: - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.7.14 + rev: 0.7.15 hooks: # Compile requirements - id: pip-compile @@ -68,7 +68,7 @@ To run the hook over multiple files at the same time, add additional entries: repos: - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.7.14 + rev: 0.7.15 hooks: # Compile requirements - id: pip-compile diff --git a/pyproject.toml b/pyproject.toml index 86dd0a1e2..5ede7780c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "uv" -version = "0.7.14" +version = "0.7.15" description = "An extremely fast Python package and project manager, written in Rust." authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }] requires-python = ">=3.8" From 4b348512c270d4fc45d4c5ab55df0bae4e2d93cd Mon Sep 17 00:00:00 2001 From: Daniel Vianna <1708810+dmvianna@users.noreply.github.com> Date: Thu, 26 Jun 2025 01:35:41 +1000 Subject: [PATCH 088/185] GCP Artifact Registry download URLs must have /simple path (#14251) --- docs/guides/integration/alternative-indexes.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/guides/integration/alternative-indexes.md b/docs/guides/integration/alternative-indexes.md index 52ec6e365..0258e4a74 100644 --- a/docs/guides/integration/alternative-indexes.md +++ b/docs/guides/integration/alternative-indexes.md @@ -142,7 +142,7 @@ To use Google Artifact Registry, add the index to your project: ```toml title="pyproject.toml" [[tool.uv.index]] name = "private-registry" -url = "https://-python.pkg.dev//" +url = "https://-python.pkg.dev///simple/" ``` ### Authenticate with a Google access token @@ -219,8 +219,8 @@ First, add a `publish-url` to the index you want to publish packages to. For exa ```toml title="pyproject.toml" hl_lines="4" [[tool.uv.index]] name = "private-registry" -url = "https://-python.pkg.dev//" -publish-url = "https://-python.pkg.dev//" +url = "https://-python.pkg.dev///simple/" +publish-url = "https://-python.pkg.dev///" ``` Then, configure credentials (if not using keyring): @@ -239,7 +239,7 @@ $ uv publish --index private-registry To use `uv publish` without adding the `publish-url` to the project, you can set `UV_PUBLISH_URL`: ```console -$ export UV_PUBLISH_URL=https://-python.pkg.dev// +$ export UV_PUBLISH_URL=https://-python.pkg.dev/// $ uv publish ``` From a27e60a22fb3cc891083c8c322e946125e0bbd62 Mon Sep 17 00:00:00 2001 From: John Mumm Date: Thu, 26 Jun 2025 16:47:18 +0200 Subject: [PATCH 089/185] Temporarily disable Artifactory registry test (#14276) I'm waiting on a response to get our subscription back up. Then I can re-enable this. But for now, this would cause failing CI tests. --- .github/workflows/ci.yml | 6 +++--- scripts/registries-test.py | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b33f96fe2..0ef7244d5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1480,9 +1480,9 @@ jobs: run: ./uv run -p ${{ env.PYTHON_VERSION }} scripts/registries-test.py --uv ./uv --color always --all env: RUST_LOG: uv=debug - UV_TEST_ARTIFACTORY_TOKEN: ${{ secrets.UV_TEST_ARTIFACTORY_TOKEN }} - UV_TEST_ARTIFACTORY_URL: ${{ secrets.UV_TEST_ARTIFACTORY_URL }} - UV_TEST_ARTIFACTORY_USERNAME: ${{ secrets.UV_TEST_ARTIFACTORY_USERNAME }} + # UV_TEST_ARTIFACTORY_TOKEN: ${{ secrets.UV_TEST_ARTIFACTORY_TOKEN }} + # UV_TEST_ARTIFACTORY_URL: ${{ secrets.UV_TEST_ARTIFACTORY_URL }} + # UV_TEST_ARTIFACTORY_USERNAME: ${{ secrets.UV_TEST_ARTIFACTORY_USERNAME }} UV_TEST_AWS_URL: ${{ secrets.UV_TEST_AWS_URL }} UV_TEST_AWS_USERNAME: aws UV_TEST_AZURE_TOKEN: ${{ secrets.UV_TEST_AZURE_TOKEN }} diff --git a/scripts/registries-test.py b/scripts/registries-test.py index 2d4c1d2aa..b6bbf9b59 100644 --- a/scripts/registries-test.py +++ b/scripts/registries-test.py @@ -56,7 +56,8 @@ DEFAULT_TIMEOUT = 30 DEFAULT_PKG_NAME = "astral-registries-test-pkg" KNOWN_REGISTRIES = [ - "artifactory", + # TODO(john): Restore this when subscription starts up again + # "artifactory", "azure", "aws", "cloudsmith", From 469246d177a9e7d36d62a63b83f1befb47aff48e Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Thu, 26 Jun 2025 09:58:24 -0500 Subject: [PATCH 090/185] Fix `emit_index_annotation_multiple_indexes` test case (#14277) uv is taken on Test PyPI now, so the existing test fails --- crates/uv/tests/it/pip_compile.rs | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/crates/uv/tests/it/pip_compile.rs b/crates/uv/tests/it/pip_compile.rs index 49e78a5c9..4b4d231dd 100644 --- a/crates/uv/tests/it/pip_compile.rs +++ b/crates/uv/tests/it/pip_compile.rs @@ -12806,28 +12806,34 @@ fn emit_index_annotation_multiple_indexes() -> Result<()> { let context = TestContext::new("3.12"); let requirements_in = context.temp_dir.child("requirements.in"); - requirements_in.write_str("uv\nrequests")?; + requirements_in.write_str("httpcore\nrequests")?; uv_snapshot!(context.filters(), context.pip_compile() .arg("requirements.in") .arg("--extra-index-url") .arg("https://test.pypi.org/simple") - .arg("--emit-index-annotation"), @r###" + .arg("--emit-index-annotation"), @r" success: true exit_code: 0 ----- stdout ----- # This file was autogenerated by uv via the following command: # uv pip compile --cache-dir [CACHE_DIR] requirements.in --emit-index-annotation + certifi==2016.8.8 + # via httpcore + # from https://test.pypi.org/simple + h11==0.14.0 + # via httpcore + # from https://pypi.org/simple + httpcore==1.0.4 + # via -r requirements.in + # from https://pypi.org/simple requests==2.5.4.1 # via -r requirements.in # from https://test.pypi.org/simple - uv==0.1.24 - # via -r requirements.in - # from https://pypi.org/simple ----- stderr ----- - Resolved 2 packages in [TIME] - "### + Resolved 4 packages in [TIME] + " ); Ok(()) From 1e02008d8bf5a79a89edb7d17228370efe477606 Mon Sep 17 00:00:00 2001 From: Aria Desires Date: Thu, 26 Jun 2025 12:05:45 -0400 Subject: [PATCH 091/185] add more proper docker login if (#14278) --- .github/workflows/build-docker.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml index 9a2e5ba1d..272a69e9a 100644 --- a/.github/workflows/build-docker.yml +++ b/.github/workflows/build-docker.yml @@ -45,6 +45,7 @@ jobs: name: plan runs-on: ubuntu-latest outputs: + login: ${{ steps.plan.outputs.login }} push: ${{ steps.plan.outputs.push }} tag: ${{ steps.plan.outputs.tag }} action: ${{ steps.plan.outputs.action }} @@ -53,13 +54,16 @@ jobs: env: DRY_RUN: ${{ inputs.plan == '' || fromJson(inputs.plan).announcement_tag_is_implicit }} TAG: ${{ inputs.plan != '' && fromJson(inputs.plan).announcement_tag }} + IS_LOCAL_PR: ${{ github.event.pull_request.head.repo.full_name == 'astral-sh/uv' }} id: plan run: | if [ "${{ env.DRY_RUN }}" == "false" ]; then + echo "login=true" >> "$GITHUB_OUTPUT" echo "push=true" >> "$GITHUB_OUTPUT" echo "tag=${{ env.TAG }}" >> "$GITHUB_OUTPUT" echo "action=build and publish" >> "$GITHUB_OUTPUT" else + echo "login=${{ env.IS_LOCAL_PR }}" >> "$GITHUB_OUTPUT" echo "push=false" >> "$GITHUB_OUTPUT" echo "tag=dry-run" >> "$GITHUB_OUTPUT" echo "action=build" >> "$GITHUB_OUTPUT" @@ -90,6 +94,7 @@ jobs: # Login to DockerHub (when not pushing, it's to avoid rate-limiting) - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 + if: ${{ needs.docker-plan.outputs.login == 'true' }} with: username: ${{ needs.docker-plan.outputs.push == 'true' && 'astral' || 'astralshbot' }} password: ${{ needs.docker-plan.outputs.push == 'true' && secrets.DOCKERHUB_TOKEN_RW || secrets.DOCKERHUB_TOKEN_RO }} @@ -195,6 +200,7 @@ jobs: steps: # Login to DockerHub (when not pushing, it's to avoid rate-limiting) - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 + if: ${{ needs.docker-plan.outputs.login == 'true' }} with: username: ${{ needs.docker-plan.outputs.push == 'true' && 'astral' || 'astralshbot' }} password: ${{ needs.docker-plan.outputs.push == 'true' && secrets.DOCKERHUB_TOKEN_RW || secrets.DOCKERHUB_TOKEN_RO }} From d27cec78b405422310c670c9f9c2aa060c54186a Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Thu, 26 Jun 2025 11:23:37 -0500 Subject: [PATCH 092/185] Restore snapshot for `sync_dry_run` (#14274) In addition to our flake catch, keep a snapshot. Extends https://github.com/astral-sh/uv/pull/13817 --- crates/uv/tests/it/sync.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index 1ce892050..4187de957 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -8173,6 +8173,8 @@ fn sync_dry_run() -> Result<()> { + iniconfig==2.0.0 "); + // TMP: Attempt to catch this flake with verbose output + // See https://github.com/astral-sh/uv/issues/13744 let output = context.sync().arg("--dry-run").arg("-vv").output()?; let stderr = String::from_utf8_lossy(&output.stderr); assert!( @@ -8181,6 +8183,19 @@ fn sync_dry_run() -> Result<()> { stderr ); + uv_snapshot!(context.filters(), context.sync().arg("--dry-run"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Discovered existing environment at: .venv + Resolved 2 packages in [TIME] + Found up-to-date lockfile at: uv.lock + Audited 1 package in [TIME] + Would make no changes + "); + Ok(()) } From 8c27c2b494dea00ec98f002bdcb6e6dde6df90d8 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Thu, 26 Jun 2025 12:11:34 -0500 Subject: [PATCH 093/185] Add verbose output on flake for `run_groups_requires_python` (#14275) See https://github.com/astral-sh/uv/issues/14160 Same as https://github.com/astral-sh/uv/pull/13817 --- crates/uv/tests/it/run.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/crates/uv/tests/it/run.rs b/crates/uv/tests/it/run.rs index 65d13c527..c2c9bc7a4 100644 --- a/crates/uv/tests/it/run.rs +++ b/crates/uv/tests/it/run.rs @@ -4686,6 +4686,22 @@ fn run_groups_requires_python() -> Result<()> { + typing-extensions==4.10.0 "); + // TMP: Attempt to catch this flake with verbose output + // See https://github.com/astral-sh/uv/issues/14160 + let output = context + .run() + .arg("python") + .arg("-c") + .arg("import typing_extensions") + .arg("-vv") + .output()?; + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + !stderr.contains("Removed virtual environment"), + "{}", + stderr + ); + // Going back to just "dev" we shouldn't churn the venv needlessly uv_snapshot!(context.filters(), context.run() .arg("python").arg("-c").arg("import typing_extensions"), @r" From 1ff8fc09475fc405971c89efd9d72f1df526eb0b Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Thu, 26 Jun 2025 13:09:04 -0500 Subject: [PATCH 094/185] Use Flit instead of Poetry for uninstall tests (#14285) Investigating https://github.com/astral-sh/uv/issues/14158 --- crates/uv/tests/it/pip_uninstall.rs | 32 ++-- scripts/packages/flit_editable/.gitignore | 162 ++++++++++++++++++ .../flit_editable/flit_editable/__init__.py | 6 + scripts/packages/flit_editable/pyproject.toml | 17 ++ 4 files changed, 201 insertions(+), 16 deletions(-) create mode 100644 scripts/packages/flit_editable/.gitignore create mode 100644 scripts/packages/flit_editable/flit_editable/__init__.py create mode 100644 scripts/packages/flit_editable/pyproject.toml diff --git a/crates/uv/tests/it/pip_uninstall.rs b/crates/uv/tests/it/pip_uninstall.rs index 5e6cbf6f9..c72b92876 100644 --- a/crates/uv/tests/it/pip_uninstall.rs +++ b/crates/uv/tests/it/pip_uninstall.rs @@ -176,7 +176,7 @@ fn uninstall_editable_by_name() -> Result<()> { "-e {}", context .workspace_root - .join("scripts/packages/poetry_editable") + .join("scripts/packages/flit_editable") .as_os_str() .to_str() .expect("Path is valid unicode") @@ -187,22 +187,22 @@ fn uninstall_editable_by_name() -> Result<()> { .assert() .success(); - context.assert_command("import poetry_editable").success(); + context.assert_command("import flit_editable").success(); // Uninstall the editable by name. uv_snapshot!(context.filters(), context.pip_uninstall() - .arg("poetry-editable"), @r###" + .arg("flit-editable"), @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Uninstalled 1 package in [TIME] - - poetry-editable==0.1.0 (from file://[WORKSPACE]/scripts/packages/poetry_editable) + - flit-editable==0.1.0 (from file://[WORKSPACE]/scripts/packages/flit_editable) "### ); - context.assert_command("import poetry_editable").failure(); + context.assert_command("import flit_editable").failure(); Ok(()) } @@ -216,7 +216,7 @@ fn uninstall_by_path() -> Result<()> { requirements_txt.write_str( context .workspace_root - .join("scripts/packages/poetry_editable") + .join("scripts/packages/flit_editable") .as_os_str() .to_str() .expect("Path is valid unicode"), @@ -228,22 +228,22 @@ fn uninstall_by_path() -> Result<()> { .assert() .success(); - context.assert_command("import poetry_editable").success(); + context.assert_command("import flit_editable").success(); // Uninstall the editable by path. uv_snapshot!(context.filters(), context.pip_uninstall() - .arg(context.workspace_root.join("scripts/packages/poetry_editable")), @r###" + .arg(context.workspace_root.join("scripts/packages/flit_editable")), @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Uninstalled 1 package in [TIME] - - poetry-editable==0.1.0 (from file://[WORKSPACE]/scripts/packages/poetry_editable) + - flit-editable==0.1.0 (from file://[WORKSPACE]/scripts/packages/flit_editable) "### ); - context.assert_command("import poetry_editable").failure(); + context.assert_command("import flit_editable").failure(); Ok(()) } @@ -257,7 +257,7 @@ fn uninstall_duplicate_by_path() -> Result<()> { requirements_txt.write_str( context .workspace_root - .join("scripts/packages/poetry_editable") + .join("scripts/packages/flit_editable") .as_os_str() .to_str() .expect("Path is valid unicode"), @@ -269,23 +269,23 @@ fn uninstall_duplicate_by_path() -> Result<()> { .assert() .success(); - context.assert_command("import poetry_editable").success(); + context.assert_command("import flit_editable").success(); // Uninstall the editable by both path and name. uv_snapshot!(context.filters(), context.pip_uninstall() - .arg("poetry-editable") - .arg(context.workspace_root.join("scripts/packages/poetry_editable")), @r###" + .arg("flit-editable") + .arg(context.workspace_root.join("scripts/packages/flit_editable")), @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Uninstalled 1 package in [TIME] - - poetry-editable==0.1.0 (from file://[WORKSPACE]/scripts/packages/poetry_editable) + - flit-editable==0.1.0 (from file://[WORKSPACE]/scripts/packages/flit_editable) "### ); - context.assert_command("import poetry_editable").failure(); + context.assert_command("import flit_editable").failure(); Ok(()) } diff --git a/scripts/packages/flit_editable/.gitignore b/scripts/packages/flit_editable/.gitignore new file mode 100644 index 000000000..3a8816c9e --- /dev/null +++ b/scripts/packages/flit_editable/.gitignore @@ -0,0 +1,162 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm-project.org/#use-with-ide +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/scripts/packages/flit_editable/flit_editable/__init__.py b/scripts/packages/flit_editable/flit_editable/__init__.py new file mode 100644 index 000000000..4076f6c86 --- /dev/null +++ b/scripts/packages/flit_editable/flit_editable/__init__.py @@ -0,0 +1,6 @@ +def main(): + print("Hello world!") + + +if __name__ == "__main__": + main() diff --git a/scripts/packages/flit_editable/pyproject.toml b/scripts/packages/flit_editable/pyproject.toml new file mode 100644 index 000000000..02f431543 --- /dev/null +++ b/scripts/packages/flit_editable/pyproject.toml @@ -0,0 +1,17 @@ +[project] +name = "flit-editable" +version = "0.1.0" +description = "Example Flit project" +authors = [ + {name = "konstin", email = "konstin@mailbox.org"}, +] +dependencies = [] +requires-python = ">=3.11" +license = {text = "MIT"} + +[project.scripts] +flit-editable = "flit_editable:main" + +[build-system] +requires = ["flit_core>=3.4,<4"] +build-backend = "flit_core.buildapi" From 60528e3e253d9c3d14dc129df68030a0a407c50d Mon Sep 17 00:00:00 2001 From: Jack O'Connor Date: Fri, 20 Jun 2025 16:00:13 -0700 Subject: [PATCH 095/185] Annotate LockedFile with #[must_use] Standard lock guards have the same annotation, because creating them without binding them to a local variable is almost always a mistake. --- crates/uv-fs/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/uv-fs/src/lib.rs b/crates/uv-fs/src/lib.rs index ad4a883ad..dcc0f00b2 100644 --- a/crates/uv-fs/src/lib.rs +++ b/crates/uv-fs/src/lib.rs @@ -601,6 +601,7 @@ pub fn is_virtualenv_base(path: impl AsRef) -> bool { /// A file lock that is automatically released when dropped. #[derive(Debug)] +#[must_use] pub struct LockedFile(fs_err::File); impl LockedFile { From d4d6ede23b95cdd245efb57b97b4fcd6f979e97c Mon Sep 17 00:00:00 2001 From: Jack O'Connor Date: Fri, 20 Jun 2025 16:04:55 -0700 Subject: [PATCH 096/185] Lock the source tree when running setuptools, to protect concurrent builds Fixes https://github.com/astral-sh/uv/issues/13703 --- Cargo.lock | 1 + crates/uv-build-frontend/Cargo.toml | 1 + crates/uv-build-frontend/src/lib.rs | 47 +++++++++++++++++++++-------- 3 files changed, 37 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 75b6fe7e4..009b3060f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4798,6 +4798,7 @@ dependencies = [ "tokio", "toml_edit", "tracing", + "uv-cache-key", "uv-configuration", "uv-distribution", "uv-distribution-types", diff --git a/crates/uv-build-frontend/Cargo.toml b/crates/uv-build-frontend/Cargo.toml index 83f8008d9..748e7bb28 100644 --- a/crates/uv-build-frontend/Cargo.toml +++ b/crates/uv-build-frontend/Cargo.toml @@ -17,6 +17,7 @@ doctest = false workspace = true [dependencies] +uv-cache-key = { workspace = true } uv-configuration = { workspace = true } uv-distribution = { workspace = true } uv-distribution-types = { workspace = true } diff --git a/crates/uv-build-frontend/src/lib.rs b/crates/uv-build-frontend/src/lib.rs index 34037ffdd..df6fc09cf 100644 --- a/crates/uv-build-frontend/src/lib.rs +++ b/crates/uv-build-frontend/src/lib.rs @@ -27,10 +27,12 @@ use tokio::process::Command; use tokio::sync::{Mutex, Semaphore}; use tracing::{Instrument, debug, info_span, instrument}; +use uv_cache_key::cache_digest; use uv_configuration::PreviewMode; use uv_configuration::{BuildKind, BuildOutput, ConfigSettings, SourceStrategy}; use uv_distribution::BuildRequires; use uv_distribution_types::{IndexLocations, Requirement, Resolution}; +use uv_fs::LockedFile; use uv_fs::{PythonExt, Simplified}; use uv_pep440::Version; use uv_pep508::PackageName; @@ -201,6 +203,11 @@ impl Pep517Backend { {import} "#, backend_path = backend_path_encoded} } + + fn is_setuptools(&self) -> bool { + // either `setuptools.build_meta` or `setuptools.build_meta:__legacy__` + self.backend.split(':').next() == Some("setuptools.build_meta") + } } /// Uses an [`Rc`] internally, clone freely. @@ -252,6 +259,8 @@ pub struct SourceBuild { environment_variables: FxHashMap, /// Runner for Python scripts. runner: PythonRunner, + /// A file lock representing the source tree, currently only used with setuptools. + _source_tree_lock: Option, } impl SourceBuild { @@ -385,6 +394,23 @@ impl SourceBuild { OsString::from(venv.scripts()) }; + // Depending on the command, setuptools puts `*.egg-info`, `build/`, and `dist/` in the + // source tree, and concurrent invocations of setuptools using the same source dir can + // stomp on each other. We need to lock something to fix that, but we don't want to dump a + // `.lock` file into the source tree that the user will need to .gitignore. Take a global + // proxy lock instead. + let mut source_tree_lock = None; + if pep517_backend.is_setuptools() { + debug!("Locking the source tree for setuptools"); + let canonical_source_path = source_tree.canonicalize()?; + let lock_path = std::env::temp_dir().join(format!( + "uv-setuptools-{}.lock", + cache_digest(&canonical_source_path) + )); + source_tree_lock = + Some(LockedFile::acquire(lock_path, source_tree.to_string_lossy()).await?); + } + // Create the PEP 517 build environment. If build isolation is disabled, we assume the build // environment is already setup. let runner = PythonRunner::new(concurrent_builds, level); @@ -431,6 +457,7 @@ impl SourceBuild { environment_variables, modified_path, runner, + _source_tree_lock: source_tree_lock, }) } @@ -716,16 +743,12 @@ impl SourceBuild { pub async fn build(&self, wheel_dir: &Path) -> Result { // The build scripts run with the extracted root as cwd, so they need the absolute path. let wheel_dir = std::path::absolute(wheel_dir)?; - let filename = self.pep517_build(&wheel_dir, &self.pep517_backend).await?; + let filename = self.pep517_build(&wheel_dir).await?; Ok(filename) } /// Perform a PEP 517 build for a wheel or source distribution (sdist). - async fn pep517_build( - &self, - output_dir: &Path, - pep517_backend: &Pep517Backend, - ) -> Result { + async fn pep517_build(&self, output_dir: &Path) -> Result { // Write the hook output to a file so that we can read it back reliably. let outfile = self .temp_dir @@ -737,7 +760,7 @@ impl SourceBuild { BuildKind::Sdist => { debug!( r#"Calling `{}.build_{}("{}", {})`"#, - pep517_backend.backend, + self.pep517_backend.backend, self.build_kind, output_dir.escape_for_python(), self.config_settings.escape_for_python(), @@ -750,7 +773,7 @@ impl SourceBuild { with open("{}", "w") as fp: fp.write(sdist_filename) "#, - pep517_backend.backend_import(), + self.pep517_backend.backend_import(), self.build_kind, output_dir.escape_for_python(), self.config_settings.escape_for_python(), @@ -766,7 +789,7 @@ impl SourceBuild { }); debug!( r#"Calling `{}.build_{}("{}", {}, {})`"#, - pep517_backend.backend, + self.pep517_backend.backend, self.build_kind, output_dir.escape_for_python(), self.config_settings.escape_for_python(), @@ -780,7 +803,7 @@ impl SourceBuild { with open("{}", "w") as fp: fp.write(wheel_filename) "#, - pep517_backend.backend_import(), + self.pep517_backend.backend_import(), self.build_kind, output_dir.escape_for_python(), self.config_settings.escape_for_python(), @@ -810,7 +833,7 @@ impl SourceBuild { return Err(Error::from_command_output( format!( "Call to `{}.build_{}` failed", - pep517_backend.backend, self.build_kind + self.pep517_backend.backend, self.build_kind ), &output, self.level, @@ -825,7 +848,7 @@ impl SourceBuild { return Err(Error::from_command_output( format!( "Call to `{}.build_{}` failed", - pep517_backend.backend, self.build_kind + self.pep517_backend.backend, self.build_kind ), &output, self.level, From c291d4329ac31b0253dcd79fe187edd066145acc Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 26 Jun 2025 15:42:04 -0400 Subject: [PATCH 097/185] Include path or URL when failing to convert in lockfile (#14292) ## Summary E.g., in #14227, we now get: ``` error: Failed to convert URL to path: https://cdn.jsdelivr.net/pyodide/v0.27.7/full/sniffio-1.3.1-py3-none-any.whl ``` --- crates/uv-resolver/src/lock/mod.rs | 54 ++++++++++++++++++------------ 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index 0b72014a8..cee6364a9 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -2545,9 +2545,12 @@ impl Package { name: name.clone(), version: version.clone(), })?; - let file_url = - DisplaySafeUrl::from_file_path(workspace_root.join(path).join(file_path)) - .map_err(|()| LockErrorKind::PathToUrl)?; + let file_path = workspace_root.join(path).join(file_path); + let file_url = DisplaySafeUrl::from_file_path(&file_path).map_err(|()| { + LockErrorKind::PathToUrl { + path: file_path.into_boxed_path(), + } + })?; let filename = sdist .filename() .ok_or_else(|| LockErrorKind::MissingFilename { @@ -3250,7 +3253,9 @@ impl Source { Ok(Source::Registry(source)) } IndexUrl::Path(url) => { - let path = url.to_file_path().map_err(|()| LockErrorKind::UrlToPath)?; + let path = url + .to_file_path() + .map_err(|()| LockErrorKind::UrlToPath { url: url.to_url() })?; let path = relative_to(&path, root) .or_else(|_| std::path::absolute(&path)) .map_err(LockErrorKind::IndexRelativePath)?; @@ -3810,14 +3815,17 @@ impl SourceDist { })) } IndexUrl::Path(path) => { - let index_path = path.to_file_path().map_err(|()| LockErrorKind::UrlToPath)?; - let reg_dist_path = reg_dist + let index_path = path + .to_file_path() + .map_err(|()| LockErrorKind::UrlToPath { url: path.to_url() })?; + let reg_dist_url = reg_dist .file .url .to_url() - .map_err(LockErrorKind::InvalidUrl)? + .map_err(LockErrorKind::InvalidUrl)?; + let reg_dist_path = reg_dist_url .to_file_path() - .map_err(|()| LockErrorKind::UrlToPath)?; + .map_err(|()| LockErrorKind::UrlToPath { url: reg_dist_url })?; let path = relative_to(®_dist_path, index_path) .or_else(|_| std::path::absolute(®_dist_path)) .map_err(LockErrorKind::DistributionRelativePath)? @@ -4140,14 +4148,13 @@ impl Wheel { }) } IndexUrl::Path(path) => { - let index_path = path.to_file_path().map_err(|()| LockErrorKind::UrlToPath)?; - let wheel_path = wheel - .file - .url - .to_url() - .map_err(LockErrorKind::InvalidUrl)? + let index_path = path .to_file_path() - .map_err(|()| LockErrorKind::UrlToPath)?; + .map_err(|()| LockErrorKind::UrlToPath { url: path.to_url() })?; + let wheel_url = wheel.file.url.to_url().map_err(LockErrorKind::InvalidUrl)?; + let wheel_path = wheel_url + .to_file_path() + .map_err(|()| LockErrorKind::UrlToPath { url: wheel_url })?; let path = relative_to(&wheel_path, index_path) .or_else(|_| std::path::absolute(&wheel_path)) .map_err(LockErrorKind::DistributionRelativePath)? @@ -4236,9 +4243,12 @@ impl Wheel { .into()); } }; - let file_url = - DisplaySafeUrl::from_file_path(root.join(index_path).join(file_path)) - .map_err(|()| LockErrorKind::PathToUrl)?; + let file_path = root.join(index_path).join(file_path); + let file_url = DisplaySafeUrl::from_file_path(&file_path).map_err(|()| { + LockErrorKind::PathToUrl { + path: file_path.into_boxed_path(), + } + })?; let file = Box::new(uv_distribution_types::File { dist_info_metadata: false, filename: SmallString::from(filename.to_string()), @@ -5449,11 +5459,11 @@ enum LockErrorKind { VerbatimUrlError, ), /// An error that occurs when converting a path to a URL. - #[error("Failed to convert path to URL")] - PathToUrl, + #[error("Failed to convert path to URL: {path}", path = path.display().cyan())] + PathToUrl { path: Box }, /// An error that occurs when converting a URL to a path - #[error("Failed to convert URL to path")] - UrlToPath, + #[error("Failed to convert URL to path: {url}", url = url.cyan())] + UrlToPath { url: DisplaySafeUrl }, /// An error that occurs when multiple packages with the same /// name were found when identifying the root packages. #[error("Found multiple packages matching `{name}`", name = name.cyan())] From 05ab266200b47e430a3d3f5fcebd164840c0b652 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 26 Jun 2025 15:48:12 -0400 Subject: [PATCH 098/185] Avoid using path URL for workspace Git dependencies in `requirements.txt` (#14288) ## Summary Closes https://github.com/astral-sh/uv/issues/13020. --- .../uv-distribution/src/metadata/lowering.rs | 22 ++++++++----- crates/uv-redacted/src/lib.rs | 4 ++- crates/uv/tests/it/pip_compile.rs | 33 +++++++++++++++++++ 3 files changed, 50 insertions(+), 9 deletions(-) diff --git a/crates/uv-distribution/src/metadata/lowering.rs b/crates/uv-distribution/src/metadata/lowering.rs index dd0974a99..330075842 100644 --- a/crates/uv-distribution/src/metadata/lowering.rs +++ b/crates/uv-distribution/src/metadata/lowering.rs @@ -13,7 +13,7 @@ use uv_git_types::{GitReference, GitUrl, GitUrlParseError}; use uv_normalize::{ExtraName, GroupName, PackageName}; use uv_pep440::VersionSpecifiers; use uv_pep508::{MarkerTree, VerbatimUrl, VersionOrUrl, looks_like_git_repository}; -use uv_pypi_types::{ConflictItem, ParsedUrlError, VerbatimParsedUrl}; +use uv_pypi_types::{ConflictItem, ParsedGitUrl, ParsedUrlError, VerbatimParsedUrl}; use uv_redacted::DisplaySafeUrl; use uv_workspace::Workspace; use uv_workspace::pyproject::{PyProjectToml, Source, Sources}; @@ -700,17 +700,23 @@ fn path_source( }; if is_dir { if let Some(git_member) = git_member { + let git = git_member.git_source.git.clone(); let subdirectory = uv_fs::relative_to(install_path, git_member.fetch_root) .expect("Workspace member must be relative"); let subdirectory = uv_fs::normalize_path_buf(subdirectory); + let subdirectory = if subdirectory == PathBuf::new() { + None + } else { + Some(subdirectory.into_boxed_path()) + }; + let url = DisplaySafeUrl::from(ParsedGitUrl { + url: git.clone(), + subdirectory: subdirectory.clone(), + }); return Ok(RequirementSource::Git { - git: git_member.git_source.git.clone(), - subdirectory: if subdirectory == PathBuf::new() { - None - } else { - Some(subdirectory.into_boxed_path()) - }, - url, + git, + subdirectory, + url: VerbatimUrl::from_url(url), }); } diff --git a/crates/uv-redacted/src/lib.rs b/crates/uv-redacted/src/lib.rs index cd023ccbf..5c9a8e278 100644 --- a/crates/uv-redacted/src/lib.rs +++ b/crates/uv-redacted/src/lib.rs @@ -177,7 +177,9 @@ impl FromStr for DisplaySafeUrl { } fn is_ssh_git_username(url: &Url) -> bool { - matches!(url.scheme(), "ssh" | "git+ssh") && url.username() == "git" && url.password().is_none() + matches!(url.scheme(), "ssh" | "git+ssh" | "git+https") + && url.username() == "git" + && url.password().is_none() } fn display_with_redacted_credentials( diff --git a/crates/uv/tests/it/pip_compile.rs b/crates/uv/tests/it/pip_compile.rs index 4b4d231dd..5adfeb7f8 100644 --- a/crates/uv/tests/it/pip_compile.rs +++ b/crates/uv/tests/it/pip_compile.rs @@ -17621,3 +17621,36 @@ fn pubgrub_panic_double_self_dependency_extra() -> Result<()> { Ok(()) } + +/// Sync a Git repository that depends on a package within the same repository via a `path` source. +/// +/// See: +#[test] +#[cfg(feature = "git")] +fn git_path_transitive_dependency() -> Result<()> { + let context = TestContext::new("3.13"); + + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str( + r" + git+https://git@github.com/astral-sh/uv-path-dependency-test.git#subdirectory=package2 + ", + )?; + + uv_snapshot!(context.filters(), context.pip_compile().arg("requirements.in"), @r" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] requirements.in + package1 @ git+https://git@github.com/astral-sh/uv-path-dependency-test.git@28781b32cf1f260cdb2c8040628079eb265202bd#subdirectory=package1 + # via package2 + package2 @ git+https://git@github.com/astral-sh/uv-path-dependency-test.git@28781b32cf1f260cdb2c8040628079eb265202bd#subdirectory=package2 + # via -r requirements.in + + ----- stderr ----- + Resolved 2 packages in [TIME] + "); + + Ok(()) +} From 326e4497da9fc64d88a5540ed0463be1db7d65e3 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 26 Jun 2025 16:17:42 -0400 Subject: [PATCH 099/185] Allow local indexes to reference remote files (#14294) ## Summary Previously, we assumed that local indexes only referenced local files. However, it's fine for a local index (like, a `file://`-based Simple API) to reference a remote file, and in fact Pyodide operates this way. Closes https://github.com/astral-sh/uv/issues/14227. ## Test Plan Ran `UV_INDEX=$(pyodide config get package_index) cargo run add anyio`, which produced this lockfile: ```toml version = 1 revision = 2 requires-python = ">=3.13.2" [[package]] name = "anyio" version = "4.9.0" source = { registry = "../../../Library/Caches/.pyodide-xbuildenv-0.30.5/0.27.7/xbuildenv/pyodide-root/package_index" } dependencies = [ { name = "idna" }, { name = "sniffio" }, ] wheels = [ { url = "https://cdn.jsdelivr.net/pyodide/v0.27.7/full/anyio-4.9.0-py3-none-any.whl", hash = "sha256:e1d9180d4361fd71d1bc4a7007fea6cae1d18792dba9d07eaad89f2a8562f71c" }, ] [[package]] name = "foo" version = "0.1.0" source = { virtual = "." } dependencies = [ { name = "anyio" }, ] [package.metadata] requires-dist = [{ name = "anyio", specifier = ">=4.9.0" }] [[package]] name = "idna" version = "3.7" source = { registry = "../../../Library/Caches/.pyodide-xbuildenv-0.30.5/0.27.7/xbuildenv/pyodide-root/package_index" } wheels = [ { url = "https://cdn.jsdelivr.net/pyodide/v0.27.7/full/idna-3.7-py3-none-any.whl", hash = "sha256:9d4685891e3e37434e09b1becda7e96a284e660c7aea9222564d88b6c3527c09" }, ] [[package]] name = "sniffio" version = "1.3.1" source = { registry = "../../../Library/Caches/.pyodide-xbuildenv-0.30.5/0.27.7/xbuildenv/pyodide-root/package_index" } wheels = [ { url = "https://cdn.jsdelivr.net/pyodide/v0.27.7/full/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:9215f9917b34fc73152b134a3fc0a2eb0e4a49b0b956100cad75e84943412bb9" }, ] ``` --- crates/uv-resolver/src/lock/mod.rs | 198 +++++++++++++++++++---------- 1 file changed, 129 insertions(+), 69 deletions(-) diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index cee6364a9..9834ad845 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -2541,16 +2541,30 @@ impl Package { .as_ref() .expect("version for registry source"); - let file_path = sdist.path().ok_or_else(|| LockErrorKind::MissingPath { - name: name.clone(), - version: version.clone(), - })?; - let file_path = workspace_root.join(path).join(file_path); - let file_url = DisplaySafeUrl::from_file_path(&file_path).map_err(|()| { - LockErrorKind::PathToUrl { - path: file_path.into_boxed_path(), + let file_url = match sdist { + SourceDist::Url { url: file_url, .. } => { + FileLocation::AbsoluteUrl(file_url.clone()) } - })?; + SourceDist::Path { + path: file_path, .. + } => { + let file_path = workspace_root.join(path).join(file_path); + let file_url = + DisplaySafeUrl::from_file_path(&file_path).map_err(|()| { + LockErrorKind::PathToUrl { + path: file_path.into_boxed_path(), + } + })?; + FileLocation::AbsoluteUrl(UrlString::from(file_url)) + } + SourceDist::Metadata { .. } => { + return Err(LockErrorKind::MissingPath { + name: name.clone(), + version: version.clone(), + } + .into()); + } + }; let filename = sdist .filename() .ok_or_else(|| LockErrorKind::MissingFilename { @@ -2571,9 +2585,10 @@ impl Package { requires_python: None, size: sdist.size(), upload_time_utc_ms: sdist.upload_time().map(Timestamp::as_millisecond), - url: FileLocation::AbsoluteUrl(UrlString::from(file_url)), + url: file_url, yanked: None, }); + let index = IndexUrl::from( VerbatimUrl::from_absolute_path(workspace_root.join(path)) .map_err(LockErrorKind::RegistryVerbatimUrl)?, @@ -3688,14 +3703,6 @@ impl SourceDist { } } - fn path(&self) -> Option<&Path> { - match &self { - SourceDist::Metadata { .. } => None, - SourceDist::Url { .. } => None, - SourceDist::Path { path, .. } => Some(path), - } - } - pub(crate) fn hash(&self) -> Option<&Hash> { match &self { SourceDist::Metadata { metadata } => metadata.hash.as_ref(), @@ -3818,34 +3825,57 @@ impl SourceDist { let index_path = path .to_file_path() .map_err(|()| LockErrorKind::UrlToPath { url: path.to_url() })?; - let reg_dist_url = reg_dist + let url = reg_dist .file .url .to_url() .map_err(LockErrorKind::InvalidUrl)?; - let reg_dist_path = reg_dist_url - .to_file_path() - .map_err(|()| LockErrorKind::UrlToPath { url: reg_dist_url })?; - let path = relative_to(®_dist_path, index_path) - .or_else(|_| std::path::absolute(®_dist_path)) - .map_err(LockErrorKind::DistributionRelativePath)? - .into_boxed_path(); - let hash = reg_dist.file.hashes.iter().max().cloned().map(Hash::from); - let size = reg_dist.file.size; - let upload_time = reg_dist - .file - .upload_time_utc_ms - .map(Timestamp::from_millisecond) - .transpose() - .map_err(LockErrorKind::InvalidTimestamp)?; - Ok(Some(SourceDist::Path { - path, - metadata: SourceDistMetadata { - hash, - size, - upload_time, - }, - })) + + if url.scheme() == "file" { + let reg_dist_path = url + .to_file_path() + .map_err(|()| LockErrorKind::UrlToPath { url })?; + let path = relative_to(®_dist_path, index_path) + .or_else(|_| std::path::absolute(®_dist_path)) + .map_err(LockErrorKind::DistributionRelativePath)? + .into_boxed_path(); + let hash = reg_dist.file.hashes.iter().max().cloned().map(Hash::from); + let size = reg_dist.file.size; + let upload_time = reg_dist + .file + .upload_time_utc_ms + .map(Timestamp::from_millisecond) + .transpose() + .map_err(LockErrorKind::InvalidTimestamp)?; + Ok(Some(SourceDist::Path { + path, + metadata: SourceDistMetadata { + hash, + size, + upload_time, + }, + })) + } else { + let url = normalize_file_location(®_dist.file.url) + .map_err(LockErrorKind::InvalidUrl) + .map_err(LockError::from)?; + let hash = reg_dist.file.hashes.iter().max().cloned().map(Hash::from); + let size = reg_dist.file.size; + let upload_time = reg_dist + .file + .upload_time_utc_ms + .map(Timestamp::from_millisecond) + .transpose() + .map_err(LockErrorKind::InvalidTimestamp)?; + Ok(Some(SourceDist::Url { + url, + metadata: SourceDistMetadata { + hash, + size, + upload_time, + }, + })) + } } } } @@ -4152,20 +4182,42 @@ impl Wheel { .to_file_path() .map_err(|()| LockErrorKind::UrlToPath { url: path.to_url() })?; let wheel_url = wheel.file.url.to_url().map_err(LockErrorKind::InvalidUrl)?; - let wheel_path = wheel_url - .to_file_path() - .map_err(|()| LockErrorKind::UrlToPath { url: wheel_url })?; - let path = relative_to(&wheel_path, index_path) - .or_else(|_| std::path::absolute(&wheel_path)) - .map_err(LockErrorKind::DistributionRelativePath)? - .into_boxed_path(); - Ok(Wheel { - url: WheelWireSource::Path { path }, - hash: None, - size: None, - upload_time: None, - filename, - }) + + if wheel_url.scheme() == "file" { + let wheel_path = wheel_url + .to_file_path() + .map_err(|()| LockErrorKind::UrlToPath { url: wheel_url })?; + let path = relative_to(&wheel_path, index_path) + .or_else(|_| std::path::absolute(&wheel_path)) + .map_err(LockErrorKind::DistributionRelativePath)? + .into_boxed_path(); + Ok(Wheel { + url: WheelWireSource::Path { path }, + hash: None, + size: None, + upload_time: None, + filename, + }) + } else { + let url = normalize_file_location(&wheel.file.url) + .map_err(LockErrorKind::InvalidUrl) + .map_err(LockError::from)?; + let hash = wheel.file.hashes.iter().max().cloned().map(Hash::from); + let size = wheel.file.size; + let upload_time = wheel + .file + .upload_time_utc_ms + .map(Timestamp::from_millisecond) + .transpose() + .map_err(LockErrorKind::InvalidTimestamp)?; + Ok(Wheel { + url: WheelWireSource::Url { url }, + hash, + size, + filename, + upload_time, + }) + } } } } @@ -4203,8 +4255,10 @@ impl Wheel { match source { RegistrySource::Url(url) => { - let file_url = match &self.url { - WheelWireSource::Url { url } => url, + let file_location = match &self.url { + WheelWireSource::Url { url: file_url } => { + FileLocation::AbsoluteUrl(file_url.clone()) + } WheelWireSource::Path { .. } | WheelWireSource::Filename { .. } => { return Err(LockErrorKind::MissingUrl { name: filename.name, @@ -4220,7 +4274,7 @@ impl Wheel { requires_python: None, size: self.size, upload_time_utc_ms: self.upload_time.map(Timestamp::as_millisecond), - url: FileLocation::AbsoluteUrl(file_url.clone()), + url: file_location, yanked: None, }); let index = IndexUrl::from(VerbatimUrl::from_url( @@ -4233,9 +4287,21 @@ impl Wheel { }) } RegistrySource::Path(index_path) => { - let file_path = match &self.url { - WheelWireSource::Path { path } => path, - WheelWireSource::Url { .. } | WheelWireSource::Filename { .. } => { + let file_location = match &self.url { + WheelWireSource::Url { url: file_url } => { + FileLocation::AbsoluteUrl(file_url.clone()) + } + WheelWireSource::Path { path: file_path } => { + let file_path = root.join(index_path).join(file_path); + let file_url = + DisplaySafeUrl::from_file_path(&file_path).map_err(|()| { + LockErrorKind::PathToUrl { + path: file_path.into_boxed_path(), + } + })?; + FileLocation::AbsoluteUrl(UrlString::from(file_url)) + } + WheelWireSource::Filename { .. } => { return Err(LockErrorKind::MissingPath { name: filename.name, version: filename.version, @@ -4243,12 +4309,6 @@ impl Wheel { .into()); } }; - let file_path = root.join(index_path).join(file_path); - let file_url = DisplaySafeUrl::from_file_path(&file_path).map_err(|()| { - LockErrorKind::PathToUrl { - path: file_path.into_boxed_path(), - } - })?; let file = Box::new(uv_distribution_types::File { dist_info_metadata: false, filename: SmallString::from(filename.to_string()), @@ -4256,7 +4316,7 @@ impl Wheel { requires_python: None, size: self.size, upload_time_utc_ms: self.upload_time.map(Timestamp::as_millisecond), - url: FileLocation::AbsoluteUrl(UrlString::from(file_url)), + url: file_location, yanked: None, }); let index = IndexUrl::from( From 9ee34dc69b5e98a84a01461b111d66d6543001f0 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Thu, 26 Jun 2025 15:40:40 -0500 Subject: [PATCH 100/185] Fix `Indexes::new` doc (#14293) --- crates/uv-auth/src/index.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/uv-auth/src/index.rs b/crates/uv-auth/src/index.rs index e17bbd8fe..b71bc9a62 100644 --- a/crates/uv-auth/src/index.rs +++ b/crates/uv-auth/src/index.rs @@ -86,7 +86,7 @@ impl Indexes { Self(FxHashSet::default()) } - /// Create a new [`AuthIndexUrls`] from an iterator of [`AuthIndexUrl`]s. + /// Create a new [`Indexes`] instance from an iterator of [`Index`]s. pub fn from_indexes(urls: impl IntoIterator) -> Self { let mut index_urls = Self::new(); for url in urls { From efc361223c134840b7650fed1ef9ebd9577b5454 Mon Sep 17 00:00:00 2001 From: Jack O'Connor Date: Thu, 26 Jun 2025 10:20:28 -0700 Subject: [PATCH 101/185] move the test buckets dir into the canonicalized temp dir Previously we were using the XDG data dir to avoid symlinks, but there's no particular guarantee that that's not going to be a symlink too. Using the canonicalized temp dir by default is also slightly nicer for a couple reasons: It's sometimes faster (an in-memory tempfs on e.g. Arch), and it makes overriding `$TMPDIR` or `%TMP%` sufficient to control where tests put temp files, without needing to override `UV_INTERNAL__TEST_DIR` too. --- .github/workflows/setup-dev-drive.ps1 | 1 - Cargo.lock | 1 - crates/uv-static/src/env_vars.rs | 4 ---- crates/uv/Cargo.toml | 1 - crates/uv/tests/it/common/mod.rs | 26 ++++++++++---------------- 5 files changed, 10 insertions(+), 23 deletions(-) diff --git a/.github/workflows/setup-dev-drive.ps1 b/.github/workflows/setup-dev-drive.ps1 index e003cc359..474b082dc 100644 --- a/.github/workflows/setup-dev-drive.ps1 +++ b/.github/workflows/setup-dev-drive.ps1 @@ -85,7 +85,6 @@ Write-Output ` "DEV_DRIVE=$($Drive)" ` "TMP=$($Tmp)" ` "TEMP=$($Tmp)" ` - "UV_INTERNAL__TEST_DIR=$($Tmp)" ` "RUSTUP_HOME=$($Drive)/.rustup" ` "CARGO_HOME=$($Drive)/.cargo" ` "UV_WORKSPACE=$($Drive)/uv" ` diff --git a/Cargo.lock b/Cargo.lock index 009b3060f..d25d7d252 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4584,7 +4584,6 @@ dependencies = [ "ctrlc", "dotenvy", "dunce", - "etcetera", "filetime", "flate2", "fs-err 3.1.1", diff --git a/crates/uv-static/src/env_vars.rs b/crates/uv-static/src/env_vars.rs index 4a44579d7..4ac2976d9 100644 --- a/crates/uv-static/src/env_vars.rs +++ b/crates/uv-static/src/env_vars.rs @@ -359,10 +359,6 @@ impl EnvVars { #[attr_hidden] pub const UV_INTERNAL__SHOW_DERIVATION_TREE: &'static str = "UV_INTERNAL__SHOW_DERIVATION_TREE"; - /// Used to set a temporary directory for some tests. - #[attr_hidden] - pub const UV_INTERNAL__TEST_DIR: &'static str = "UV_INTERNAL__TEST_DIR"; - /// Path to system-level configuration directory on Unix systems. pub const XDG_CONFIG_DIRS: &'static str = "XDG_CONFIG_DIRS"; diff --git a/crates/uv/Cargo.toml b/crates/uv/Cargo.toml index f4afb87fa..9b73a9ebd 100644 --- a/crates/uv/Cargo.toml +++ b/crates/uv/Cargo.toml @@ -114,7 +114,6 @@ assert_cmd = { version = "2.0.16" } assert_fs = { version = "1.1.2" } base64 = { workspace = true } byteorder = { version = "1.5.0" } -etcetera = { workspace = true } filetime = { version = "0.2.25" } flate2 = { workspace = true, default-features = false } ignore = { version = "0.4.23" } diff --git a/crates/uv/tests/it/common/mod.rs b/crates/uv/tests/it/common/mod.rs index 4d65aa4a3..9ead0a7c2 100644 --- a/crates/uv/tests/it/common/mod.rs +++ b/crates/uv/tests/it/common/mod.rs @@ -13,7 +13,6 @@ use assert_cmd::assert::{Assert, OutputAssertExt}; use assert_fs::assert::PathAssert; use assert_fs::fixture::{ChildPath, PathChild, PathCopy, PathCreateDir, SymlinkToFile}; use base64::{Engine, prelude::BASE64_STANDARD as base64}; -use etcetera::BaseStrategy; use futures::StreamExt; use indoc::formatdoc; use itertools::Itertools; @@ -407,25 +406,20 @@ impl TestContext { self } - /// Discover the path to the XDG state directory. We use this, rather than the OS-specific - /// temporary directory, because on macOS (and Windows on GitHub Actions), they involve - /// symlinks. (On macOS, the temporary directory is, like `/var/...`, which resolves to - /// `/private/var/...`.) + /// Default to the canonicalized path to the temp directory. We need to do this because on + /// macOS (and Windows on GitHub Actions) the standard temp dir is a symlink. (On macOS, the + /// temporary directory is, like `/var/...`, which resolves to `/private/var/...`.) /// /// It turns out that, at least on macOS, if we pass a symlink as `current_dir`, it gets /// _immediately_ resolved (such that if you call `current_dir` in the running `Command`, it - /// returns resolved symlink). This is problematic, as we _don't_ want to resolve symlinks - /// for user-provided paths. + /// returns resolved symlink). This breaks some snapshot tests, since we _don't_ want to + /// resolve symlinks for user-provided paths. pub fn test_bucket_dir() -> PathBuf { - env::var(EnvVars::UV_INTERNAL__TEST_DIR) - .map(PathBuf::from) - .unwrap_or_else(|_| { - etcetera::base_strategy::choose_base_strategy() - .expect("Failed to find base strategy") - .data_dir() - .join("uv") - .join("tests") - }) + std::env::temp_dir() + .simple_canonicalize() + .expect("failed to canonicalize temp dir") + .join("uv") + .join("tests") } /// Create a new test context with multiple Python versions. From 56266447e228d7c042036f26e86f4fe4f46d66f2 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 27 Jun 2025 10:27:45 -0400 Subject: [PATCH 102/185] Bump MSRV and `rust-toolchain` version (#14303) ## Summary Per our versioning policy, we stay two versions back (and 1.88 was released today). --- Cargo.toml | 2 +- clippy.toml | 6 +- crates/uv-fs/src/which.rs | 2 +- crates/uv-python/src/discovery.rs | 2 +- crates/uv-trampoline-builder/src/lib.rs | 2 +- crates/uv/tests/it/ecosystem.rs | 2 +- crates/uv/tests/it/pip_compile.rs | 94 +------------------------ crates/uv/tests/it/pip_install.rs | 2 +- rust-toolchain.toml | 2 +- 9 files changed, 11 insertions(+), 103 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5f3cdf40c..817c5c62b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ resolver = "2" [workspace.package] edition = "2024" -rust-version = "1.85" +rust-version = "1.86" homepage = "https://pypi.org/project/uv/" documentation = "https://pypi.org/project/uv/" repository = "https://github.com/astral-sh/uv" diff --git a/clippy.toml b/clippy.toml index 6b3031c84..1151d773d 100644 --- a/clippy.toml +++ b/clippy.toml @@ -37,7 +37,7 @@ disallowed-methods = [ "std::fs::soft_link", "std::fs::symlink_metadata", "std::fs::write", - "std::os::unix::fs::symlink", - "std::os::windows::fs::symlink_dir", - "std::os::windows::fs::symlink_file", + { path = "std::os::unix::fs::symlink", allow-invalid = true }, + { path = "std::os::windows::fs::symlink_dir", allow-invalid = true }, + { path = "std::os::windows::fs::symlink_file", allow-invalid = true }, ] diff --git a/crates/uv-fs/src/which.rs b/crates/uv-fs/src/which.rs index 9dd4cc508..e63174a17 100644 --- a/crates/uv-fs/src/which.rs +++ b/crates/uv-fs/src/which.rs @@ -17,7 +17,7 @@ fn get_binary_type(path: &Path) -> windows::core::Result { .chain(Some(0)) .collect::>(); // SAFETY: winapi call - unsafe { GetBinaryTypeW(PCWSTR(name.as_ptr()), &mut binary_type)? }; + unsafe { GetBinaryTypeW(PCWSTR(name.as_ptr()), &raw mut binary_type)? }; Ok(binary_type) } diff --git a/crates/uv-python/src/discovery.rs b/crates/uv-python/src/discovery.rs index d1f3a690a..67f8f37ff 100644 --- a/crates/uv-python/src/discovery.rs +++ b/crates/uv-python/src/discovery.rs @@ -1433,7 +1433,7 @@ pub(crate) fn is_windows_store_shim(path: &Path) -> bool { 0, buf.as_mut_ptr().cast(), buf.len() as u32 * 2, - &mut bytes_returned, + &raw mut bytes_returned, std::ptr::null_mut(), ) != 0 }; diff --git a/crates/uv-trampoline-builder/src/lib.rs b/crates/uv-trampoline-builder/src/lib.rs index 15b435ec5..2e1cde872 100644 --- a/crates/uv-trampoline-builder/src/lib.rs +++ b/crates/uv-trampoline-builder/src/lib.rs @@ -521,7 +521,7 @@ if __name__ == "__main__": } #[test] - #[ignore] + #[ignore = "This test will spawn a GUI and wait until you close the window."] fn gui_launcher() -> Result<()> { // Create Temp Dirs let temp_dir = assert_fs::TempDir::new()?; diff --git a/crates/uv/tests/it/ecosystem.rs b/crates/uv/tests/it/ecosystem.rs index e96dca62c..a3804f426 100644 --- a/crates/uv/tests/it/ecosystem.rs +++ b/crates/uv/tests/it/ecosystem.rs @@ -73,8 +73,8 @@ fn saleor() -> Result<()> { // Currently ignored because the project doesn't build with `uv` yet. // // Source: https://github.com/apache/airflow/blob/c55438d9b2eb9b6680641eefdd0cbc67a28d1d29/pyproject.toml -#[ignore] #[test] +#[ignore = "Airflow doesn't build with `uv` yet"] fn airflow() -> Result<()> { lock_ecosystem_package("3.12", "airflow") } diff --git a/crates/uv/tests/it/pip_compile.rs b/crates/uv/tests/it/pip_compile.rs index 5adfeb7f8..79a98a3bf 100644 --- a/crates/uv/tests/it/pip_compile.rs +++ b/crates/uv/tests/it/pip_compile.rs @@ -3,9 +3,8 @@ use std::env::current_dir; use std::fs; use std::io::Cursor; -use std::path::PathBuf; -use anyhow::{Context, Result, bail}; +use anyhow::Result; use assert_fs::prelude::*; use flate2::write::GzEncoder; use fs_err::File; @@ -4803,97 +4802,6 @@ fn compile_editable_url_requirement() -> Result<()> { Ok(()) } -#[test] -#[ignore] -fn cache_errors_are_non_fatal() -> Result<()> { - let context = TestContext::new("3.12"); - let requirements_in = context.temp_dir.child("requirements.in"); - // No git dep, git has its own locking strategy - requirements_in.write_str(indoc! {r" - # pypi wheel - pandas - # url wheel - flask @ https://files.pythonhosted.org/packages/36/42/015c23096649b908c809c69388a805a571a3bea44362fe87e33fc3afa01f/flask-3.0.0-py3-none-any.whl - # url source dist - werkzeug @ https://files.pythonhosted.org/packages/0d/cc/ff1904eb5eb4b455e442834dabf9427331ac0fa02853bf83db817a7dd53d/werkzeug-3.0.1.tar.gz - " - })?; - - // Pick a file from each kind of cache - let interpreter_cache = context - .cache_dir - .path() - .join("interpreter-v0") - .read_dir()? - .next() - .context("Expected a python interpreter cache file")?? - .path(); - let cache_files = [ - PathBuf::from("simple-v0/pypi/numpy.msgpack"), - PathBuf::from( - "wheels-v0/pypi/python-dateutil/python_dateutil-2.8.2-py2.py3-none-any.msgpack", - ), - PathBuf::from("wheels-v0/url/4b8be67c801a7ecb/flask/flask-3.0.0-py3-none-any.msgpack"), - PathBuf::from("built-wheels-v0/url/6781bd6440ae72c2/werkzeug/metadata.msgpack"), - interpreter_cache, - ]; - - let check = || { - uv_snapshot!(context.filters(), context.pip_compile() - .arg("pip") - .arg("compile") - .arg(requirements_in.path()) - // It's sufficient to check that we resolve to a fix number of packages - .stdout(std::process::Stdio::null()), @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 13 packages in [TIME] - "### - ); - }; - - insta::allow_duplicates! { - check(); - - // Replace some cache files with invalid contents - for file in &cache_files { - let file = context.cache_dir.join(file); - if !file.is_file() { - bail!("Missing cache file {}", file.user_display()); - } - fs_err::write(file, "I borken you cache")?; - } - - check(); - - #[cfg(unix)] - { - use fs_err::os::unix::fs::OpenOptionsExt; - - // Make some files unreadable, so that the read instead of the deserialization will fail - for file in cache_files { - let file = context.cache_dir.join(file); - if !file.is_file() { - bail!("Missing cache file {}", file.user_display()); - } - - fs_err::OpenOptions::new() - .create(true) - .write(true) - .mode(0o000) - .open(file)?; - } - } - - check(); - - Ok(()) - } -} - /// Resolve a distribution from an HTML-only registry. #[test] #[cfg(not(target_env = "musl"))] // No musllinux wheels in the torch index diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index 604b8db15..090fb03a9 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -2344,7 +2344,7 @@ fn install_git_private_https_pat_at_ref() { /// See: . #[test] #[cfg(feature = "git")] -#[ignore] +#[ignore = "Modifies the user's keyring"] fn install_git_private_https_pat_and_username() { let context = TestContext::new(DEFAULT_PYTHON_VERSION); let token = decode_token(common::READ_ONLY_GITHUB_TOKEN); diff --git a/rust-toolchain.toml b/rust-toolchain.toml index e7f22fb8b..c95c90571 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "1.86" +channel = "1.88" From a824468c8b4055be54747e201a12f8f33e7db12d Mon Sep 17 00:00:00 2001 From: John Mumm Date: Fri, 27 Jun 2025 16:41:14 +0200 Subject: [PATCH 103/185] Respect URL-encoded credentials in redirect location (#14315) uv currently ignores URL-encoded credentials in a redirect location. This PR adds a check for these credentials to the redirect handling logic. If found, they are moved to the Authorization header in the redirect request. Closes #11097 --- crates/uv-client/src/base_client.rs | 11 +++++ crates/uv/tests/it/edit.rs | 63 ++++++++++++++++++++++++++++- 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/crates/uv-client/src/base_client.rs b/crates/uv-client/src/base_client.rs index 85c384b0d..d62621863 100644 --- a/crates/uv-client/src/base_client.rs +++ b/crates/uv-client/src/base_client.rs @@ -25,6 +25,7 @@ use tracing::{debug, trace}; use url::ParseError; use url::Url; +use uv_auth::Credentials; use uv_auth::{AuthMiddleware, Indexes}; use uv_configuration::{KeyringProviderType, TrustedHost}; use uv_fs::Simplified; @@ -725,6 +726,16 @@ fn request_into_redirect( } } + // Check if there are credentials on the redirect location itself. + // If so, move them to Authorization header. + if !redirect_url.username().is_empty() { + if let Some(credentials) = Credentials::from_url(&redirect_url) { + let _ = redirect_url.set_username(""); + let _ = redirect_url.set_password(None); + headers.insert(AUTHORIZATION, credentials.to_header_value()); + } + } + std::mem::swap(req.headers_mut(), &mut headers); *req.url_mut() = Url::from(redirect_url); debug!( diff --git a/crates/uv/tests/it/edit.rs b/crates/uv/tests/it/edit.rs index 2f9c91e84..3c26ab342 100644 --- a/crates/uv/tests/it/edit.rs +++ b/crates/uv/tests/it/edit.rs @@ -12018,6 +12018,61 @@ async fn add_redirect_cross_origin() -> Result<()> { Ok(()) } +/// If uv receives a 302 redirect to a cross-origin server with credentials +/// in the location, use those credentials for the redirect request. +#[tokio::test] +async fn add_redirect_cross_origin_credentials_in_location() -> Result<()> { + let context = TestContext::new("3.12"); + let filters = context + .filters() + .into_iter() + .chain([(r"127\.0\.0\.1:\d*", "[LOCALHOST]")]) + .collect::>(); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! { r#" + [project] + name = "foo" + version = "1.0.0" + requires-python = ">=3.12" + dependencies = [] + "# + })?; + + let redirect_server = MockServer::start().await; + + Mock::given(method("GET")) + .respond_with(|req: &wiremock::Request| { + // Responds with credentials in the location + let redirect_url = redirect_url_to_base( + req, + "https://public:heron@pypi-proxy.fly.dev/basic-auth/simple/", + ); + ResponseTemplate::new(302).insert_header("Location", &redirect_url) + }) + .mount(&redirect_server) + .await; + + let redirect_url = Url::parse(&redirect_server.uri())?; + + uv_snapshot!(filters, context.add().arg("--default-index").arg(redirect_url.as_str()).arg("anyio"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 4 packages in [TIME] + Prepared 3 packages in [TIME] + Installed 3 packages in [TIME] + + anyio==4.3.0 + + idna==3.6 + + sniffio==1.3.1 + " + ); + + Ok(()) +} + /// uv currently fails to look up keyring credentials on a cross-origin redirect. #[tokio::test] async fn add_redirect_with_keyring_cross_origin() -> Result<()> { @@ -12145,14 +12200,18 @@ async fn pip_install_redirect_with_netrc_cross_origin() -> Result<()> { } fn redirect_url_to_pypi_proxy(req: &wiremock::Request) -> String { + redirect_url_to_base(req, "https://pypi-proxy.fly.dev/basic-auth/simple/") +} + +fn redirect_url_to_base(req: &wiremock::Request, base: &str) -> String { let last_path_segment = req .url .path_segments() .expect("path has segments") - .filter(|segment| !segment.is_empty()) // Filter out empty segments + .filter(|segment| !segment.is_empty()) .next_back() .expect("path has a package segment"); - format!("https://pypi-proxy.fly.dev/basic-auth/simple/{last_path_segment}/") + format!("{base}{last_path_segment}/") } /// Test the error message when adding a package with multiple existing references in From 5754f2f2db03824f3f6c6b915e508c09d141088b Mon Sep 17 00:00:00 2001 From: John Mumm Date: Fri, 27 Jun 2025 17:11:21 +0200 Subject: [PATCH 104/185] Normalize index URLs to remove trailing slash (#14245) This PR updates `IndexUrl` parsing to normalize non-file URLs by removing trailing slashes. It also normalizes registry source URLs when using them to validate the lockfile. Prior to this change, when writing an index URL to the lockfile, uv would use a trailing slash if present in the provided URL and no trailing slash otherwise. This can cause surprising behavior. For example, `uv lock --locked` will fail when a package is added with an `--index` value without a trailing slash and then `uv lock --locked` is run with a `pyproject.toml` version of the index URL that contains a trailing slash. This PR fixes this and adds a test for the scenario. It might be safe to normalize file URLs in the same way, but since slashes have a well-defined meaning in the context of files and directories, I chose not to normalize them here. Closes #13707. --- crates/uv-distribution-types/src/file.rs | 11 + crates/uv-distribution-types/src/index_url.rs | 15 +- crates/uv-resolver/src/lock/mod.rs | 6 +- crates/uv/tests/it/edit.rs | 24 +- crates/uv/tests/it/lock.rs | 201 +++++++++++- crates/uv/tests/it/lock_scenarios.rs | 306 +++++++++--------- crates/uv/tests/it/sync.rs | 2 +- 7 files changed, 391 insertions(+), 174 deletions(-) diff --git a/crates/uv-distribution-types/src/file.rs b/crates/uv-distribution-types/src/file.rs index e17901f80..2d30eb0f2 100644 --- a/crates/uv-distribution-types/src/file.rs +++ b/crates/uv-distribution-types/src/file.rs @@ -171,6 +171,17 @@ impl UrlString { .unwrap_or_else(|| self.0.clone()), ) } + + /// Return the [`UrlString`] with trailing slash removed. + #[must_use] + pub fn without_trailing_slash(&self) -> Self { + Self( + self.as_ref() + .strip_suffix('/') + .map(SmallString::from) + .unwrap_or_else(|| self.0.clone()), + ) + } } impl AsRef for UrlString { diff --git a/crates/uv-distribution-types/src/index_url.rs b/crates/uv-distribution-types/src/index_url.rs index 2b686c9fe..90b5ad809 100644 --- a/crates/uv-distribution-types/src/index_url.rs +++ b/crates/uv-distribution-types/src/index_url.rs @@ -38,13 +38,22 @@ impl IndexUrl { /// /// If no root directory is provided, relative paths are resolved against the current working /// directory. + /// + /// Normalizes non-file URLs by removing trailing slashes for consistency. pub fn parse(path: &str, root_dir: Option<&Path>) -> Result { let url = match split_scheme(path) { Some((scheme, ..)) => { match Scheme::parse(scheme) { - Some(_) => { - // Ex) `https://pypi.org/simple` - VerbatimUrl::parse_url(path)? + Some(scheme) => { + if scheme.is_file() { + // Ex) `file:///path/to/something/` + VerbatimUrl::parse_url(path)? + } else { + // Ex) `https://pypi.org/simple/` + // Remove a trailing slash if it exists. + let normalized_path = path.strip_suffix('/').unwrap_or(path); + VerbatimUrl::parse_url(normalized_path)? + } } None => { // Ex) `C:\Users\user\index` diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index 9834ad845..8ff3097de 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -1478,9 +1478,11 @@ impl Lock { if let Source::Registry(index) = &package.id.source { match index { RegistrySource::Url(url) => { + // Normalize URL before validating. + let url = url.without_trailing_slash(); if remotes .as_ref() - .is_some_and(|remotes| !remotes.contains(url)) + .is_some_and(|remotes| !remotes.contains(&url)) { let name = &package.id.name; let version = &package @@ -1793,7 +1795,7 @@ pub enum SatisfiesResult<'lock> { /// The lockfile is missing a workspace member. MissingRoot(PackageName), /// The lockfile referenced a remote index that was not provided - MissingRemoteIndex(&'lock PackageName, &'lock Version, &'lock UrlString), + MissingRemoteIndex(&'lock PackageName, &'lock Version, UrlString), /// The lockfile referenced a local index that was not provided MissingLocalIndex(&'lock PackageName, &'lock Version, &'lock Path), /// A package in the lockfile contains different `requires-dist` metadata than expected. diff --git a/crates/uv/tests/it/edit.rs b/crates/uv/tests/it/edit.rs index 3c26ab342..ee0bf04ee 100644 --- a/crates/uv/tests/it/edit.rs +++ b/crates/uv/tests/it/edit.rs @@ -4374,7 +4374,7 @@ fn add_lower_bound_local() -> Result<()> { filters => context.filters(), }, { assert_snapshot!( - pyproject_toml, @r###" + pyproject_toml, @r#" [project] name = "project" version = "0.1.0" @@ -4384,8 +4384,8 @@ fn add_lower_bound_local() -> Result<()> { ] [[tool.uv.index]] - url = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" - "### + url = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" + "# ); }); @@ -4403,7 +4403,7 @@ fn add_lower_bound_local() -> Result<()> { [[package]] name = "local-simple-a" version = "1.2.3+foo" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } sdist = { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/local_simple_a-1.2.3+foo.tar.gz", hash = "sha256:ebd55c4a79d0a5759126657cb289ff97558902abcfb142e036b993781497edac" } wheels = [ { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/local_simple_a-1.2.3+foo-py3-none-any.whl", hash = "sha256:6f30e2e709b3e171cd734bb58705229a582587c29e0a7041227435583c7224cc" }, @@ -9265,7 +9265,7 @@ fn add_index_with_trailing_slash() -> Result<()> { filters => context.filters(), }, { assert_snapshot!( - pyproject_toml, @r###" + pyproject_toml, @r#" [project] name = "project" version = "0.1.0" @@ -9278,8 +9278,8 @@ fn add_index_with_trailing_slash() -> Result<()> { constraint-dependencies = ["markupsafe<3"] [[tool.uv.index]] - url = "https://pypi.org/simple/" - "### + url = "https://pypi.org/simple" + "# ); }); @@ -9303,7 +9303,7 @@ fn add_index_with_trailing_slash() -> Result<()> { [[package]] name = "iniconfig" version = "2.0.0" - source = { registry = "https://pypi.org/simple/" } + source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646, upload-time = "2023-01-07T11:08:11.254Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892, upload-time = "2023-01-07T11:08:09.864Z" }, @@ -11200,7 +11200,7 @@ fn repeated_index_cli_reversed() -> Result<()> { filters => context.filters(), }, { assert_snapshot!( - pyproject_toml, @r###" + pyproject_toml, @r#" [project] name = "project" version = "0.1.0" @@ -11210,8 +11210,8 @@ fn repeated_index_cli_reversed() -> Result<()> { ] [[tool.uv.index]] - url = "https://test.pypi.org/simple/" - "### + url = "https://test.pypi.org/simple" + "# ); }); @@ -11232,7 +11232,7 @@ fn repeated_index_cli_reversed() -> Result<()> { [[package]] name = "iniconfig" version = "2.0.0" - source = { registry = "https://test.pypi.org/simple/" } + source = { registry = "https://test.pypi.org/simple" } sdist = { url = "https://test-files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646, upload-time = "2023-01-07T11:08:16.826Z" } wheels = [ { url = "https://test-files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892, upload-time = "2023-01-07T11:08:14.843Z" }, diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index ac20124a0..e0fc8749d 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -15479,7 +15479,7 @@ fn lock_trailing_slash() -> Result<()> { [[package]] name = "anyio" version = "3.7.0" - source = { registry = "https://pypi.org/simple/" } + source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { name = "sniffio" }, @@ -15492,7 +15492,7 @@ fn lock_trailing_slash() -> Result<()> { [[package]] name = "idna" version = "3.6" - source = { registry = "https://pypi.org/simple/" } + source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426, upload-time = "2023-11-25T15:40:54.902Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567, upload-time = "2023-11-25T15:40:52.604Z" }, @@ -15512,7 +15512,7 @@ fn lock_trailing_slash() -> Result<()> { [[package]] name = "sniffio" version = "1.3.1" - source = { registry = "https://pypi.org/simple/" } + source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, @@ -28257,3 +28257,198 @@ fn lock_conflict_for_disjoint_platform() -> Result<()> { Ok(()) } + +/// Add a package with an `--index` URL with no trailing slash. Run `uv lock --locked` +/// with a `pyproject.toml` with that same URL but with a trailing slash. +#[test] +fn lock_with_inconsistent_trailing_slash() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + + [[tool.uv.index]] + name = "pypi-proxy" + url = "https://pypi-proxy.fly.dev/simple/" + "#, + )?; + + let no_trailing_slash_url = "https://pypi-proxy.fly.dev/simple"; + + uv_snapshot!(context.filters(), context.add().arg("anyio").arg("--index").arg(no_trailing_slash_url), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 4 packages in [TIME] + Prepared 3 packages in [TIME] + Installed 3 packages in [TIME] + + anyio==4.3.0 + + idna==3.6 + + sniffio==1.3.1 + "); + + let lock = context.read("uv.lock"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r#" + version = 1 + revision = 2 + requires-python = ">=3.12" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [[package]] + name = "anyio" + version = "4.3.0" + source = { registry = "https://pypi-proxy.fly.dev/simple" } + dependencies = [ + { name = "idna" }, + { name = "sniffio" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6", size = 159642, upload-time = "2024-02-19T08:36:28.641Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8", size = 85584, upload-time = "2024-02-19T08:36:26.842Z" }, + ] + + [[package]] + name = "idna" + version = "3.6" + source = { registry = "https://pypi-proxy.fly.dev/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426, upload-time = "2023-11-25T15:40:54.902Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567, upload-time = "2023-11-25T15:40:52.604Z" }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "anyio" }, + ] + + [package.metadata] + requires-dist = [{ name = "anyio", specifier = ">=4.3.0" }] + + [[package]] + name = "sniffio" + version = "1.3.1" + source = { registry = "https://pypi-proxy.fly.dev/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, + ] + "# + ); + }); + + // Re-run with `--locked`. + uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 4 packages in [TIME] + "); + + Ok(()) +} + +/// Run `uv lock --locked` with a lockfile with trailing slashes on index URLs. +#[test] +fn lock_with_index_trailing_slashes_in_lockfile() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["anyio"] + + [[tool.uv.index]] + name = "pypi-proxy" + url = "https://pypi-proxy.fly.dev/simple" + "#, + )?; + + let lock = context.temp_dir.child("uv.lock"); + lock.write_str(r#" + version = 1 + revision = 2 + requires-python = ">=3.12" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [[package]] + name = "anyio" + version = "4.3.0" + source = { registry = "https://pypi-proxy.fly.dev/simple/" } + dependencies = [ + { name = "idna" }, + { name = "sniffio" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6", size = 159642, upload-time = "2024-02-19T08:36:28.641Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8", size = 85584, upload-time = "2024-02-19T08:36:26.842Z" }, + ] + + [[package]] + name = "idna" + version = "3.6" + source = { registry = "https://pypi-proxy.fly.dev/simple/" } + sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426, upload-time = "2023-11-25T15:40:54.902Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567, upload-time = "2023-11-25T15:40:52.604Z" }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "anyio" }, + ] + + [package.metadata] + requires-dist = [{ name = "anyio" }] + + [[package]] + name = "sniffio" + version = "1.3.1" + source = { registry = "https://pypi-proxy.fly.dev/simple/" } + sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, + ] + "# + )?; + + // Run `uv lock --locked`. + uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 4 packages in [TIME] + "); + + Ok(()) +} diff --git a/crates/uv/tests/it/lock_scenarios.rs b/crates/uv/tests/it/lock_scenarios.rs index 3be986ad1..801214fa5 100644 --- a/crates/uv/tests/it/lock_scenarios.rs +++ b/crates/uv/tests/it/lock_scenarios.rs @@ -158,7 +158,7 @@ fn wrong_backtracking_basic() -> Result<()> { [[package]] name = "package-a" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } sdist = { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/wrong_backtracking_basic_a-1.0.0.tar.gz", hash = "sha256:5251a827291d4e5b7ca11c742df3aa26802cc55442e3f5fc307ff3423b8f9295" } wheels = [ { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/wrong_backtracking_basic_a-1.0.0-py3-none-any.whl", hash = "sha256:d9a7ee79b176cd36c9db03e36bc3325856dd4fb061aefc6159eecad6e8776e88" }, @@ -167,7 +167,7 @@ fn wrong_backtracking_basic() -> Result<()> { [[package]] name = "package-b" version = "2.0.9" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } dependencies = [ { name = "package-a" }, ] @@ -340,7 +340,7 @@ fn wrong_backtracking_indirect() -> Result<()> { [[package]] name = "package-a" version = "2.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } sdist = { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/wrong_backtracking_indirect_a-2.0.0.tar.gz", hash = "sha256:5891b5a45aac67b3afb90f66913d7ced2ada7cad1676fe427136b7324935bb1e" } wheels = [ { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/wrong_backtracking_indirect_a-2.0.0-py3-none-any.whl", hash = "sha256:68cb37193f4b2277630ad083522f59ac0449cb1c59e943884d04cc0e2a04cba7" }, @@ -349,7 +349,7 @@ fn wrong_backtracking_indirect() -> Result<()> { [[package]] name = "package-b" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } dependencies = [ { name = "package-b-inner" }, ] @@ -361,7 +361,7 @@ fn wrong_backtracking_indirect() -> Result<()> { [[package]] name = "package-b-inner" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } dependencies = [ { name = "package-too-old" }, ] @@ -373,7 +373,7 @@ fn wrong_backtracking_indirect() -> Result<()> { [[package]] name = "package-too-old" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } sdist = { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/wrong_backtracking_indirect_too_old-1.0.0.tar.gz", hash = "sha256:1b674a931c34e29d20f22e9b92206b648769fa9e35770ab680466dbaa1335090" } wheels = [ { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/wrong_backtracking_indirect_too_old-1.0.0-py3-none-any.whl", hash = "sha256:15f8fe39323691c883c3088f8873220944428210a74db080f60a61a74c1fc6b0" }, @@ -477,7 +477,7 @@ fn fork_allows_non_conflicting_non_overlapping_dependencies() -> Result<()> { [[package]] name = "package-a" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } sdist = { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/fork_allows_non_conflicting_non_overlapping_dependencies_a-1.0.0.tar.gz", hash = "sha256:dd40a6bd59fbeefbf9f4936aec3df6fb6017e57d334f85f482ae5dd03ae353b9" } wheels = [ { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/fork_allows_non_conflicting_non_overlapping_dependencies_a-1.0.0-py3-none-any.whl", hash = "sha256:8111e996c2a4e04c7a7cf91cf6f8338f5195c22ecf2303d899c4ef4e718a8175" }, @@ -592,7 +592,7 @@ fn fork_allows_non_conflicting_repeated_dependencies() -> Result<()> { [[package]] name = "package-a" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } sdist = { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/fork_allows_non_conflicting_repeated_dependencies_a-1.0.0.tar.gz", hash = "sha256:45ca30f1f66eaf6790198fad279b6448719092f2128f23b99f2ede0d6dde613b" } wheels = [ { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/fork_allows_non_conflicting_repeated_dependencies_a-1.0.0-py3-none-any.whl", hash = "sha256:fc3f6d2fab10d1bb4f52bd9a7de69dc97ed1792506706ca78bdc9e95d6641a6b" }, @@ -699,7 +699,7 @@ fn fork_basic() -> Result<()> { [[package]] name = "package-a" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } resolution-markers = [ "sys_platform == 'darwin'", ] @@ -711,7 +711,7 @@ fn fork_basic() -> Result<()> { [[package]] name = "package-a" version = "2.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } resolution-markers = [ "sys_platform == 'linux'", ] @@ -725,8 +725,8 @@ fn fork_basic() -> Result<()> { version = "0.1.0" source = { virtual = "." } dependencies = [ - { name = "package-a", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'darwin'" }, - { name = "package-a", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'linux'" }, + { name = "package-a", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" }, marker = "sys_platform == 'darwin'" }, + { name = "package-a", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" }, marker = "sys_platform == 'linux'" }, ] [package.metadata] @@ -1002,7 +1002,7 @@ fn fork_filter_sibling_dependencies() -> Result<()> { [[package]] name = "package-a" version = "4.3.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } resolution-markers = [ "sys_platform == 'darwin'", ] @@ -1014,7 +1014,7 @@ fn fork_filter_sibling_dependencies() -> Result<()> { [[package]] name = "package-a" version = "4.4.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } resolution-markers = [ "sys_platform == 'linux'", ] @@ -1026,9 +1026,9 @@ fn fork_filter_sibling_dependencies() -> Result<()> { [[package]] name = "package-b" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } dependencies = [ - { name = "package-d", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'linux'" }, + { name = "package-d", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" }, marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/fork_filter_sibling_dependencies_b-1.0.0.tar.gz", hash = "sha256:af3f861d6df9a2bbad55bae02acf17384ea2efa1abbf19206ac56cb021814613" } wheels = [ @@ -1038,9 +1038,9 @@ fn fork_filter_sibling_dependencies() -> Result<()> { [[package]] name = "package-c" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } dependencies = [ - { name = "package-d", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'darwin'" }, + { name = "package-d", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" }, marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/fork_filter_sibling_dependencies_c-1.0.0.tar.gz", hash = "sha256:c03742ca6e81c2a5d7d8cb72d1214bf03b2925e63858a19097f17d3e1a750192" } wheels = [ @@ -1050,7 +1050,7 @@ fn fork_filter_sibling_dependencies() -> Result<()> { [[package]] name = "package-d" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } resolution-markers = [ "sys_platform == 'linux'", ] @@ -1062,7 +1062,7 @@ fn fork_filter_sibling_dependencies() -> Result<()> { [[package]] name = "package-d" version = "2.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } resolution-markers = [ "sys_platform == 'darwin'", ] @@ -1076,8 +1076,8 @@ fn fork_filter_sibling_dependencies() -> Result<()> { version = "0.1.0" source = { virtual = "." } dependencies = [ - { name = "package-a", version = "4.3.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'darwin'" }, - { name = "package-a", version = "4.4.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'linux'" }, + { name = "package-a", version = "4.3.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" }, marker = "sys_platform == 'darwin'" }, + { name = "package-a", version = "4.4.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" }, marker = "sys_platform == 'linux'" }, { name = "package-b", marker = "sys_platform == 'linux'" }, { name = "package-c", marker = "sys_platform == 'darwin'" }, ] @@ -1180,7 +1180,7 @@ fn fork_upgrade() -> Result<()> { [[package]] name = "package-bar" version = "2.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } sdist = { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/fork_upgrade_bar-2.0.0.tar.gz", hash = "sha256:2e7b5370d7be19b5af56092a8364a2718a7b8516142a12a95656b82d1b9c8cbc" } wheels = [ { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/fork_upgrade_bar-2.0.0-py3-none-any.whl", hash = "sha256:d8ce562bf363e849fbf4add170a519b5412ab63e378fb4b7ea290183c77616fc" }, @@ -1189,7 +1189,7 @@ fn fork_upgrade() -> Result<()> { [[package]] name = "package-foo" version = "2.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } dependencies = [ { name = "package-bar" }, ] @@ -1310,7 +1310,7 @@ fn fork_incomplete_markers() -> Result<()> { [[package]] name = "package-a" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } resolution-markers = [ "python_full_version < '3.10'", ] @@ -1322,7 +1322,7 @@ fn fork_incomplete_markers() -> Result<()> { [[package]] name = "package-a" version = "2.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } resolution-markers = [ "python_full_version >= '3.11'", ] @@ -1334,7 +1334,7 @@ fn fork_incomplete_markers() -> Result<()> { [[package]] name = "package-b" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } dependencies = [ { name = "package-c", marker = "python_full_version == '3.10.*'" }, ] @@ -1346,7 +1346,7 @@ fn fork_incomplete_markers() -> Result<()> { [[package]] name = "package-c" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } sdist = { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/fork_incomplete_markers_c-1.0.0.tar.gz", hash = "sha256:ecc02ea1cc8d3b561c8dcb9d2ba1abcdae2dd32de608bf8e8ed2878118426022" } wheels = [ { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/fork_incomplete_markers_c-1.0.0-py3-none-any.whl", hash = "sha256:03fa287aa4cb78457211cb3df7459b99ba1ee2259aae24bc745eaab45e7eaaee" }, @@ -1357,8 +1357,8 @@ fn fork_incomplete_markers() -> Result<()> { version = "0.1.0" source = { virtual = "." } dependencies = [ - { name = "package-a", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "python_full_version < '3.10'" }, - { name = "package-a", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "python_full_version >= '3.11'" }, + { name = "package-a", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" }, marker = "python_full_version < '3.10'" }, + { name = "package-a", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" }, marker = "python_full_version >= '3.11'" }, { name = "package-b" }, ] @@ -1462,7 +1462,7 @@ fn fork_marker_accrue() -> Result<()> { [[package]] name = "package-a" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } dependencies = [ { name = "package-c", marker = "sys_platform == 'linux'" }, ] @@ -1474,7 +1474,7 @@ fn fork_marker_accrue() -> Result<()> { [[package]] name = "package-b" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } dependencies = [ { name = "package-c", marker = "sys_platform == 'darwin'" }, ] @@ -1486,7 +1486,7 @@ fn fork_marker_accrue() -> Result<()> { [[package]] name = "package-c" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } sdist = { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/fork_marker_accrue_c-1.0.0.tar.gz", hash = "sha256:a3e09ac3dc8e787a08ebe8d5d6072e09720c76cbbcb76a6645d6f59652742015" } wheels = [ { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/fork_marker_accrue_c-1.0.0-py3-none-any.whl", hash = "sha256:b0c8719d38c91b2a8548bd065b1d2153fbe031b37775ed244e76fe5bdfbb502e" }, @@ -1680,15 +1680,15 @@ fn fork_marker_inherit_combined_allowed() -> Result<()> { [[package]] name = "package-a" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } resolution-markers = [ "implementation_name == 'pypy' and sys_platform == 'darwin'", "implementation_name == 'cpython' and sys_platform == 'darwin'", "implementation_name != 'cpython' and implementation_name != 'pypy' and sys_platform == 'darwin'", ] dependencies = [ - { name = "package-b", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "implementation_name == 'pypy' and sys_platform == 'darwin'" }, - { name = "package-b", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "implementation_name == 'cpython' and sys_platform == 'darwin'" }, + { name = "package-b", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" }, marker = "implementation_name == 'pypy' and sys_platform == 'darwin'" }, + { name = "package-b", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" }, marker = "implementation_name == 'cpython' and sys_platform == 'darwin'" }, ] sdist = { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/fork_marker_inherit_combined_allowed_a-1.0.0.tar.gz", hash = "sha256:c7232306e8597d46c3fe53a3b1472f99b8ff36b3169f335ba0a5b625e193f7d4" } wheels = [ @@ -1698,7 +1698,7 @@ fn fork_marker_inherit_combined_allowed() -> Result<()> { [[package]] name = "package-a" version = "2.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } resolution-markers = [ "sys_platform == 'linux'", ] @@ -1710,7 +1710,7 @@ fn fork_marker_inherit_combined_allowed() -> Result<()> { [[package]] name = "package-b" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } resolution-markers = [ "implementation_name == 'pypy' and sys_platform == 'darwin'", ] @@ -1725,7 +1725,7 @@ fn fork_marker_inherit_combined_allowed() -> Result<()> { [[package]] name = "package-b" version = "2.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } resolution-markers = [ "implementation_name == 'cpython' and sys_platform == 'darwin'", ] @@ -1737,7 +1737,7 @@ fn fork_marker_inherit_combined_allowed() -> Result<()> { [[package]] name = "package-c" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } sdist = { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/fork_marker_inherit_combined_allowed_c-1.0.0.tar.gz", hash = "sha256:7ce8efca029cfa952e64f55c2d47fe33975c7f77ec689384bda11cbc3b7ef1db" } wheels = [ { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/fork_marker_inherit_combined_allowed_c-1.0.0-py3-none-any.whl", hash = "sha256:6a6b776dedabceb6a6c4f54a5d932076fa3fed1380310491999ca2d31e13b41c" }, @@ -1748,8 +1748,8 @@ fn fork_marker_inherit_combined_allowed() -> Result<()> { version = "0.1.0" source = { virtual = "." } dependencies = [ - { name = "package-a", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'darwin'" }, - { name = "package-a", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'linux'" }, + { name = "package-a", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" }, marker = "sys_platform == 'darwin'" }, + { name = "package-a", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" }, marker = "sys_platform == 'linux'" }, ] [package.metadata] @@ -1866,15 +1866,15 @@ fn fork_marker_inherit_combined_disallowed() -> Result<()> { [[package]] name = "package-a" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } resolution-markers = [ "implementation_name == 'pypy' and sys_platform == 'darwin'", "implementation_name == 'cpython' and sys_platform == 'darwin'", "implementation_name != 'cpython' and implementation_name != 'pypy' and sys_platform == 'darwin'", ] dependencies = [ - { name = "package-b", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "implementation_name == 'pypy' and sys_platform == 'darwin'" }, - { name = "package-b", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "implementation_name == 'cpython' and sys_platform == 'darwin'" }, + { name = "package-b", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" }, marker = "implementation_name == 'pypy' and sys_platform == 'darwin'" }, + { name = "package-b", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" }, marker = "implementation_name == 'cpython' and sys_platform == 'darwin'" }, ] sdist = { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/fork_marker_inherit_combined_disallowed_a-1.0.0.tar.gz", hash = "sha256:92081d91570582f3a94ed156f203de53baca5b3fdc350aa1c831c7c42723e798" } wheels = [ @@ -1884,7 +1884,7 @@ fn fork_marker_inherit_combined_disallowed() -> Result<()> { [[package]] name = "package-a" version = "2.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } resolution-markers = [ "sys_platform == 'linux'", ] @@ -1896,7 +1896,7 @@ fn fork_marker_inherit_combined_disallowed() -> Result<()> { [[package]] name = "package-b" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } resolution-markers = [ "implementation_name == 'pypy' and sys_platform == 'darwin'", ] @@ -1908,7 +1908,7 @@ fn fork_marker_inherit_combined_disallowed() -> Result<()> { [[package]] name = "package-b" version = "2.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } resolution-markers = [ "implementation_name == 'cpython' and sys_platform == 'darwin'", ] @@ -1922,8 +1922,8 @@ fn fork_marker_inherit_combined_disallowed() -> Result<()> { version = "0.1.0" source = { virtual = "." } dependencies = [ - { name = "package-a", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'darwin'" }, - { name = "package-a", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'linux'" }, + { name = "package-a", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" }, marker = "sys_platform == 'darwin'" }, + { name = "package-a", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" }, marker = "sys_platform == 'linux'" }, ] [package.metadata] @@ -2041,15 +2041,15 @@ fn fork_marker_inherit_combined() -> Result<()> { [[package]] name = "package-a" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } resolution-markers = [ "implementation_name == 'pypy' and sys_platform == 'darwin'", "implementation_name == 'cpython' and sys_platform == 'darwin'", "implementation_name != 'cpython' and implementation_name != 'pypy' and sys_platform == 'darwin'", ] dependencies = [ - { name = "package-b", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "implementation_name == 'pypy' and sys_platform == 'darwin'" }, - { name = "package-b", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "implementation_name == 'cpython' and sys_platform == 'darwin'" }, + { name = "package-b", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" }, marker = "implementation_name == 'pypy' and sys_platform == 'darwin'" }, + { name = "package-b", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" }, marker = "implementation_name == 'cpython' and sys_platform == 'darwin'" }, ] sdist = { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/fork_marker_inherit_combined_a-1.0.0.tar.gz", hash = "sha256:2ec4c9dbb7078227d996c344b9e0c1b365ed0000de9527b2ba5b616233636f07" } wheels = [ @@ -2059,7 +2059,7 @@ fn fork_marker_inherit_combined() -> Result<()> { [[package]] name = "package-a" version = "2.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } resolution-markers = [ "sys_platform == 'linux'", ] @@ -2071,7 +2071,7 @@ fn fork_marker_inherit_combined() -> Result<()> { [[package]] name = "package-b" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } resolution-markers = [ "implementation_name == 'pypy' and sys_platform == 'darwin'", ] @@ -2083,7 +2083,7 @@ fn fork_marker_inherit_combined() -> Result<()> { [[package]] name = "package-b" version = "2.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } resolution-markers = [ "implementation_name == 'cpython' and sys_platform == 'darwin'", ] @@ -2097,8 +2097,8 @@ fn fork_marker_inherit_combined() -> Result<()> { version = "0.1.0" source = { virtual = "." } dependencies = [ - { name = "package-a", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'darwin'" }, - { name = "package-a", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'linux'" }, + { name = "package-a", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" }, marker = "sys_platform == 'darwin'" }, + { name = "package-a", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" }, marker = "sys_platform == 'linux'" }, ] [package.metadata] @@ -2205,7 +2205,7 @@ fn fork_marker_inherit_isolated() -> Result<()> { [[package]] name = "package-a" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } resolution-markers = [ "sys_platform == 'darwin'", ] @@ -2217,7 +2217,7 @@ fn fork_marker_inherit_isolated() -> Result<()> { [[package]] name = "package-a" version = "2.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } resolution-markers = [ "sys_platform == 'linux'", ] @@ -2232,7 +2232,7 @@ fn fork_marker_inherit_isolated() -> Result<()> { [[package]] name = "package-b" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } sdist = { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/fork_marker_inherit_isolated_b-1.0.0.tar.gz", hash = "sha256:96f8c3cabc5795e08a064c89ec76a4bfba8afe3c13d647161b4a1568b4584ced" } wheels = [ { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/fork_marker_inherit_isolated_b-1.0.0-py3-none-any.whl", hash = "sha256:c8affc2f13f9bcd08b3d1601a21a1781ea14d52a8cddc708b29428c9c3d53ea5" }, @@ -2243,8 +2243,8 @@ fn fork_marker_inherit_isolated() -> Result<()> { version = "0.1.0" source = { virtual = "." } dependencies = [ - { name = "package-a", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'darwin'" }, - { name = "package-a", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'linux'" }, + { name = "package-a", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" }, marker = "sys_platform == 'darwin'" }, + { name = "package-a", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" }, marker = "sys_platform == 'linux'" }, ] [package.metadata] @@ -2359,7 +2359,7 @@ fn fork_marker_inherit_transitive() -> Result<()> { [[package]] name = "package-a" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } resolution-markers = [ "sys_platform == 'darwin'", ] @@ -2374,7 +2374,7 @@ fn fork_marker_inherit_transitive() -> Result<()> { [[package]] name = "package-a" version = "2.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } resolution-markers = [ "sys_platform == 'linux'", ] @@ -2386,7 +2386,7 @@ fn fork_marker_inherit_transitive() -> Result<()> { [[package]] name = "package-b" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } dependencies = [ { name = "package-c", marker = "sys_platform == 'darwin'" }, ] @@ -2398,7 +2398,7 @@ fn fork_marker_inherit_transitive() -> Result<()> { [[package]] name = "package-c" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } sdist = { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/fork_marker_inherit_transitive_c-1.0.0.tar.gz", hash = "sha256:58bb788896b2297f2948f51a27fc48cfe44057c687a3c0c4d686b107975f7f32" } wheels = [ { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/fork_marker_inherit_transitive_c-1.0.0-py3-none-any.whl", hash = "sha256:ad2cbb0582ec6f4dc9549d1726d2aae66cd1fdf0e355acc70cd720cf65ae4d86" }, @@ -2409,8 +2409,8 @@ fn fork_marker_inherit_transitive() -> Result<()> { version = "0.1.0" source = { virtual = "." } dependencies = [ - { name = "package-a", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'darwin'" }, - { name = "package-a", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'linux'" }, + { name = "package-a", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" }, marker = "sys_platform == 'darwin'" }, + { name = "package-a", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" }, marker = "sys_platform == 'linux'" }, ] [package.metadata] @@ -2519,7 +2519,7 @@ fn fork_marker_inherit() -> Result<()> { [[package]] name = "package-a" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } resolution-markers = [ "sys_platform == 'darwin'", ] @@ -2531,7 +2531,7 @@ fn fork_marker_inherit() -> Result<()> { [[package]] name = "package-a" version = "2.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } resolution-markers = [ "sys_platform == 'linux'", ] @@ -2545,8 +2545,8 @@ fn fork_marker_inherit() -> Result<()> { version = "0.1.0" source = { virtual = "." } dependencies = [ - { name = "package-a", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'darwin'" }, - { name = "package-a", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'linux'" }, + { name = "package-a", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" }, marker = "sys_platform == 'darwin'" }, + { name = "package-a", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" }, marker = "sys_platform == 'linux'" }, ] [package.metadata] @@ -2662,7 +2662,7 @@ fn fork_marker_limited_inherit() -> Result<()> { [[package]] name = "package-a" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } resolution-markers = [ "sys_platform == 'darwin'", ] @@ -2674,7 +2674,7 @@ fn fork_marker_limited_inherit() -> Result<()> { [[package]] name = "package-a" version = "2.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } resolution-markers = [ "sys_platform == 'linux'", ] @@ -2686,7 +2686,7 @@ fn fork_marker_limited_inherit() -> Result<()> { [[package]] name = "package-b" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } dependencies = [ { name = "package-c", marker = "sys_platform == 'linux'" }, ] @@ -2698,7 +2698,7 @@ fn fork_marker_limited_inherit() -> Result<()> { [[package]] name = "package-c" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } sdist = { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/fork_marker_limited_inherit_c-1.0.0.tar.gz", hash = "sha256:8dcb05f5dff09fec52ab507b215ff367fe815848319a17929db997ad3afe88ae" } wheels = [ { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/fork_marker_limited_inherit_c-1.0.0-py3-none-any.whl", hash = "sha256:877a87a4987ad795ddaded3e7266ed7defdd3cfbe07a29500cb6047637db4065" }, @@ -2709,8 +2709,8 @@ fn fork_marker_limited_inherit() -> Result<()> { version = "0.1.0" source = { virtual = "." } dependencies = [ - { name = "package-a", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'darwin'" }, - { name = "package-a", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'linux'" }, + { name = "package-a", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" }, marker = "sys_platform == 'darwin'" }, + { name = "package-a", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" }, marker = "sys_platform == 'linux'" }, { name = "package-b" }, ] @@ -2822,7 +2822,7 @@ fn fork_marker_selection() -> Result<()> { [[package]] name = "package-a" version = "0.1.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } sdist = { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/fork_marker_selection_a-0.1.0.tar.gz", hash = "sha256:ece83ba864a62d5d747439f79a0bf36aa4c18d15bca96aab855ffc2e94a8eef7" } wheels = [ { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/fork_marker_selection_a-0.1.0-py3-none-any.whl", hash = "sha256:a3b9d6e46cc226d20994cc60653fd59d81d96527749f971a6f59ef8cbcbc7c01" }, @@ -2831,7 +2831,7 @@ fn fork_marker_selection() -> Result<()> { [[package]] name = "package-b" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } resolution-markers = [ "sys_platform == 'darwin'", ] @@ -2843,7 +2843,7 @@ fn fork_marker_selection() -> Result<()> { [[package]] name = "package-b" version = "2.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } resolution-markers = [ "sys_platform == 'linux'", ] @@ -2858,8 +2858,8 @@ fn fork_marker_selection() -> Result<()> { source = { virtual = "." } dependencies = [ { name = "package-a" }, - { name = "package-b", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'darwin'" }, - { name = "package-b", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'linux'" }, + { name = "package-b", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" }, marker = "sys_platform == 'darwin'" }, + { name = "package-b", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" }, marker = "sys_platform == 'linux'" }, ] [package.metadata] @@ -2985,7 +2985,7 @@ fn fork_marker_track() -> Result<()> { [[package]] name = "package-a" version = "1.3.1" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } dependencies = [ { name = "package-c", marker = "implementation_name == 'iron'" }, ] @@ -2997,7 +2997,7 @@ fn fork_marker_track() -> Result<()> { [[package]] name = "package-b" version = "2.7" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } resolution-markers = [ "sys_platform == 'darwin'", ] @@ -3009,7 +3009,7 @@ fn fork_marker_track() -> Result<()> { [[package]] name = "package-b" version = "2.8" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } resolution-markers = [ "sys_platform == 'linux'", ] @@ -3021,7 +3021,7 @@ fn fork_marker_track() -> Result<()> { [[package]] name = "package-c" version = "1.10" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } sdist = { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/fork_marker_track_c-1.10.tar.gz", hash = "sha256:c89006d893254790b0fcdd1b33520241c8ff66ab950c6752b745e006bdeff144" } wheels = [ { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/fork_marker_track_c-1.10-py3-none-any.whl", hash = "sha256:cedcb8fbcdd9fbde4eea76612e57536c8b56507a9d7f7a92e483cb56b18c57a3" }, @@ -3033,8 +3033,8 @@ fn fork_marker_track() -> Result<()> { source = { virtual = "." } dependencies = [ { name = "package-a" }, - { name = "package-b", version = "2.7", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'darwin'" }, - { name = "package-b", version = "2.8", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'linux'" }, + { name = "package-b", version = "2.7", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" }, marker = "sys_platform == 'darwin'" }, + { name = "package-b", version = "2.8", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" }, marker = "sys_platform == 'linux'" }, ] [package.metadata] @@ -3137,7 +3137,7 @@ fn fork_non_fork_marker_transitive() -> Result<()> { [[package]] name = "package-a" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } dependencies = [ { name = "package-c", marker = "sys_platform == 'linux'" }, ] @@ -3149,7 +3149,7 @@ fn fork_non_fork_marker_transitive() -> Result<()> { [[package]] name = "package-b" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } dependencies = [ { name = "package-c", marker = "sys_platform == 'darwin'" }, ] @@ -3161,7 +3161,7 @@ fn fork_non_fork_marker_transitive() -> Result<()> { [[package]] name = "package-c" version = "2.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } sdist = { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/fork_non_fork_marker_transitive_c-2.0.0.tar.gz", hash = "sha256:ffab9124854f64c8b5059ccaed481547f54abac868ba98aa6a454c0163cdb1c7" } wheels = [ { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/fork_non_fork_marker_transitive_c-2.0.0-py3-none-any.whl", hash = "sha256:2b72d6af81967e1c55f30d920d6a7b913fce6ad0a0658ec79972a3d1a054e85f" }, @@ -3453,7 +3453,7 @@ fn fork_overlapping_markers_basic() -> Result<()> { [[package]] name = "package-a" version = "1.2.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } sdist = { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/fork_overlapping_markers_basic_a-1.2.0.tar.gz", hash = "sha256:f8c2058d80430d62b15c87fd66040a6c0dd23d32e7f144a932899c0c74bdff2a" } wheels = [ { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/fork_overlapping_markers_basic_a-1.2.0-py3-none-any.whl", hash = "sha256:04293ed42eb3620c9ddf56e380a8408a30733d5d38f321a35c024d03e7116083" }, @@ -3636,11 +3636,11 @@ fn preferences_dependent_forking_bistable() -> Result<()> { [[package]] name = "package-cleaver" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } dependencies = [ - { name = "package-fork-if-not-forked", version = "3.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'linux'" }, + { name = "package-fork-if-not-forked", version = "3.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" }, marker = "sys_platform == 'linux'" }, { name = "package-fork-if-not-forked-proxy", marker = "sys_platform != 'linux'" }, - { name = "package-reject-cleaver1", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'linux'" }, + { name = "package-reject-cleaver1", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" }, marker = "sys_platform == 'linux'" }, { name = "package-reject-cleaver1-proxy" }, ] sdist = { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/preferences_dependent_forking_bistable_cleaver-1.0.0.tar.gz", hash = "sha256:64e5ee0c81d6a51fb71ed517fd04cc26c656908ad05073270e67c2f9b92194c5" } @@ -3651,7 +3651,7 @@ fn preferences_dependent_forking_bistable() -> Result<()> { [[package]] name = "package-fork-if-not-forked" version = "2.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } resolution-markers = [ "sys_platform != 'linux'", ] @@ -3663,7 +3663,7 @@ fn preferences_dependent_forking_bistable() -> Result<()> { [[package]] name = "package-fork-if-not-forked" version = "3.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } resolution-markers = [ "sys_platform == 'linux'", ] @@ -3675,9 +3675,9 @@ fn preferences_dependent_forking_bistable() -> Result<()> { [[package]] name = "package-fork-if-not-forked-proxy" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } dependencies = [ - { name = "package-fork-if-not-forked", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform != 'linux'" }, + { name = "package-fork-if-not-forked", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" }, marker = "sys_platform != 'linux'" }, ] sdist = { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/preferences_dependent_forking_bistable_fork_if_not_forked_proxy-1.0.0.tar.gz", hash = "sha256:0ed00a7c8280348225835fadc76db8ecc6b4a9ee11351a6c432c475f8d1579de" } wheels = [ @@ -3687,7 +3687,7 @@ fn preferences_dependent_forking_bistable() -> Result<()> { [[package]] name = "package-reject-cleaver1" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } resolution-markers = [ "sys_platform == 'linux'", ] @@ -3699,7 +3699,7 @@ fn preferences_dependent_forking_bistable() -> Result<()> { [[package]] name = "package-reject-cleaver1" version = "2.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } resolution-markers = [ "sys_platform != 'linux'", ] @@ -3711,9 +3711,9 @@ fn preferences_dependent_forking_bistable() -> Result<()> { [[package]] name = "package-reject-cleaver1-proxy" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } dependencies = [ - { name = "package-reject-cleaver1", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform != 'linux'" }, + { name = "package-reject-cleaver1", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" }, marker = "sys_platform != 'linux'" }, ] sdist = { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/preferences_dependent_forking_bistable_reject_cleaver1_proxy-1.0.0.tar.gz", hash = "sha256:6b6eaa229d55de992e36084521d2f62dce35120a866e20354d0e5617e16e00ce" } wheels = [ @@ -4048,7 +4048,7 @@ fn preferences_dependent_forking_tristable() -> Result<()> { [[package]] name = "package-bar" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } resolution-markers = [ "sys_platform != 'linux'", ] @@ -4064,7 +4064,7 @@ fn preferences_dependent_forking_tristable() -> Result<()> { [[package]] name = "package-bar" version = "2.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } resolution-markers = [ "sys_platform == 'linux'", ] @@ -4076,7 +4076,7 @@ fn preferences_dependent_forking_tristable() -> Result<()> { [[package]] name = "package-c" version = "2.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } resolution-markers = [ "sys_platform == 'linux'", ] @@ -4088,7 +4088,7 @@ fn preferences_dependent_forking_tristable() -> Result<()> { [[package]] name = "package-c" version = "3.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } resolution-markers = [ "sys_platform != 'linux'", ] @@ -4100,9 +4100,9 @@ fn preferences_dependent_forking_tristable() -> Result<()> { [[package]] name = "package-cleaver" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } dependencies = [ - { name = "package-bar", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform != 'linux'" }, + { name = "package-bar", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" }, marker = "sys_platform != 'linux'" }, { name = "package-foo", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/preferences_dependent_forking_tristable_cleaver-1.0.0.tar.gz", hash = "sha256:49ec5779d0722586652e3ceb4ca2bf053a79dc3fa2d7ccd428a359bcc885a248" } @@ -4113,9 +4113,9 @@ fn preferences_dependent_forking_tristable() -> Result<()> { [[package]] name = "package-d" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } dependencies = [ - { name = "package-c", version = "3.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform != 'linux'" }, + { name = "package-c", version = "3.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" }, marker = "sys_platform != 'linux'" }, ] sdist = { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/preferences_dependent_forking_tristable_d-1.0.0.tar.gz", hash = "sha256:690b69acb46d0ebfb11a81f401d2ea2e2e6a8ae97f199d345715e9bd40a7ceba" } wheels = [ @@ -4125,10 +4125,10 @@ fn preferences_dependent_forking_tristable() -> Result<()> { [[package]] name = "package-foo" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } dependencies = [ - { name = "package-c", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'linux'" }, - { name = "package-c", version = "3.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform != 'linux'" }, + { name = "package-c", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" }, marker = "sys_platform == 'linux'" }, + { name = "package-c", version = "3.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" }, marker = "sys_platform != 'linux'" }, { name = "package-reject-cleaver-1" }, ] sdist = { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/preferences_dependent_forking_tristable_foo-1.0.0.tar.gz", hash = "sha256:7c1a2ca51dd2156cf36c3400e38595e11b09442052f4bd1d6b3d53eb5b2acf32" } @@ -4139,10 +4139,10 @@ fn preferences_dependent_forking_tristable() -> Result<()> { [[package]] name = "package-reject-cleaver-1" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } dependencies = [ - { name = "package-unrelated-dep2", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'linux'" }, - { name = "package-unrelated-dep2", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform != 'linux'" }, + { name = "package-unrelated-dep2", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" }, marker = "sys_platform == 'linux'" }, + { name = "package-unrelated-dep2", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" }, marker = "sys_platform != 'linux'" }, ] sdist = { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/preferences_dependent_forking_tristable_reject_cleaver_1-1.0.0.tar.gz", hash = "sha256:6ef93ca22db3a054559cb34f574ffa3789951f2f82b213c5502d0e9ff746f15e" } wheels = [ @@ -4152,7 +4152,7 @@ fn preferences_dependent_forking_tristable() -> Result<()> { [[package]] name = "package-unrelated-dep2" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } resolution-markers = [ "sys_platform == 'linux'", ] @@ -4164,7 +4164,7 @@ fn preferences_dependent_forking_tristable() -> Result<()> { [[package]] name = "package-unrelated-dep2" version = "2.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } resolution-markers = [ "sys_platform != 'linux'", ] @@ -4178,8 +4178,8 @@ fn preferences_dependent_forking_tristable() -> Result<()> { version = "0.1.0" source = { virtual = "." } dependencies = [ - { name = "package-bar", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform != 'linux'" }, - { name = "package-bar", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'linux'" }, + { name = "package-bar", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" }, marker = "sys_platform != 'linux'" }, + { name = "package-bar", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" }, marker = "sys_platform == 'linux'" }, { name = "package-cleaver" }, { name = "package-foo" }, ] @@ -4342,7 +4342,7 @@ fn preferences_dependent_forking() -> Result<()> { [[package]] name = "package-bar" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } resolution-markers = [ "sys_platform != 'linux'", ] @@ -4354,7 +4354,7 @@ fn preferences_dependent_forking() -> Result<()> { [[package]] name = "package-bar" version = "2.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } resolution-markers = [ "sys_platform == 'linux'", ] @@ -4366,9 +4366,9 @@ fn preferences_dependent_forking() -> Result<()> { [[package]] name = "package-cleaver" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } dependencies = [ - { name = "package-bar", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform != 'linux'" }, + { name = "package-bar", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" }, marker = "sys_platform != 'linux'" }, { name = "package-foo", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/preferences_dependent_forking_cleaver-1.0.0.tar.gz", hash = "sha256:0347b927fdf7731758ea53e1594309fc6311ca6983f36553bc11654a264062b2" } @@ -4379,7 +4379,7 @@ fn preferences_dependent_forking() -> Result<()> { [[package]] name = "package-foo" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } sdist = { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/preferences_dependent_forking_foo-1.0.0.tar.gz", hash = "sha256:abf1c0ac825ee5961e683067634916f98c6651a6d4473ff87d8b57c17af8fed2" } wheels = [ { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/preferences_dependent_forking_foo-1.0.0-py3-none-any.whl", hash = "sha256:85348e8df4892b9f297560c16abcf231828f538dc07339ed121197a00a0626a5" }, @@ -4390,8 +4390,8 @@ fn preferences_dependent_forking() -> Result<()> { version = "0.1.0" source = { virtual = "." } dependencies = [ - { name = "package-bar", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform != 'linux'" }, - { name = "package-bar", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'linux'" }, + { name = "package-bar", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" }, marker = "sys_platform != 'linux'" }, + { name = "package-bar", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" }, marker = "sys_platform == 'linux'" }, { name = "package-cleaver" }, { name = "package-foo" }, ] @@ -4525,15 +4525,15 @@ fn fork_remaining_universe_partitioning() -> Result<()> { [[package]] name = "package-a" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } resolution-markers = [ "os_name == 'darwin' and sys_platform == 'illumos'", "os_name == 'linux' and sys_platform == 'illumos'", "os_name != 'darwin' and os_name != 'linux' and sys_platform == 'illumos'", ] dependencies = [ - { name = "package-b", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "os_name == 'darwin' and sys_platform == 'illumos'" }, - { name = "package-b", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "os_name == 'linux' and sys_platform == 'illumos'" }, + { name = "package-b", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" }, marker = "os_name == 'darwin' and sys_platform == 'illumos'" }, + { name = "package-b", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" }, marker = "os_name == 'linux' and sys_platform == 'illumos'" }, ] sdist = { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/fork_remaining_universe_partitioning_a-1.0.0.tar.gz", hash = "sha256:d5be0af9a1958ec08ca2827b47bfd507efc26cab03ecf7ddf204e18e8a3a18ae" } wheels = [ @@ -4543,7 +4543,7 @@ fn fork_remaining_universe_partitioning() -> Result<()> { [[package]] name = "package-a" version = "2.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } resolution-markers = [ "sys_platform == 'windows'", ] @@ -4555,7 +4555,7 @@ fn fork_remaining_universe_partitioning() -> Result<()> { [[package]] name = "package-b" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } resolution-markers = [ "os_name == 'darwin' and sys_platform == 'illumos'", ] @@ -4567,7 +4567,7 @@ fn fork_remaining_universe_partitioning() -> Result<()> { [[package]] name = "package-b" version = "2.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } resolution-markers = [ "os_name == 'linux' and sys_platform == 'illumos'", ] @@ -4581,8 +4581,8 @@ fn fork_remaining_universe_partitioning() -> Result<()> { version = "0.1.0" source = { virtual = "." } dependencies = [ - { name = "package-a", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'illumos'" }, - { name = "package-a", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'windows'" }, + { name = "package-a", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" }, marker = "sys_platform == 'illumos'" }, + { name = "package-a", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" }, marker = "sys_platform == 'windows'" }, ] [package.metadata] @@ -4845,7 +4845,7 @@ fn fork_requires_python_patch_overlap() -> Result<()> { [[package]] name = "package-a" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } sdist = { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/fork_requires_python_patch_overlap_a-1.0.0.tar.gz", hash = "sha256:ac2820ee4808788674295192d79a709e3259aa4eef5b155e77f719ad4eaa324d" } wheels = [ { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/fork_requires_python_patch_overlap_a-1.0.0-py3-none-any.whl", hash = "sha256:43a750ba4eaab749d608d70e94d3d51e083cc21f5a52ac99b5967b26486d5ef1" }, @@ -5031,7 +5031,7 @@ fn requires_python_wheels() -> Result<()> { [[package]] name = "package-a" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } sdist = { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/requires_python_wheels_a-1.0.0.tar.gz", hash = "sha256:9a11ff73fdc513c4dab0d3e137f4145a00ef0dfc95154360c8f503eed62a03c9" } wheels = [ { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/requires_python_wheels_a-1.0.0-cp310-cp310-any.whl", hash = "sha256:b979494a0d7dc825b84d6c516ac407143915f6d2840d229ee2a36b3d06deb61d" }, @@ -5130,7 +5130,7 @@ fn unreachable_package() -> Result<()> { [[package]] name = "package-a" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } sdist = { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/unreachable_package_a-1.0.0.tar.gz", hash = "sha256:308f0b6772e99dcb33acee38003b176e3acffbe01c3c511585db9a7d7ec008f7" } wheels = [ { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/unreachable_package_a-1.0.0-py3-none-any.whl", hash = "sha256:cc472ded9f3b260e6cda0e633fa407a13607e190422cb455f02beebd32d6751f" }, @@ -5241,7 +5241,7 @@ fn unreachable_wheels() -> Result<()> { [[package]] name = "package-a" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } sdist = { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/unreachable_wheels_a-1.0.0.tar.gz", hash = "sha256:91c6619d1cfa227f3662c0c062b1c0c16efe11e589db2f1836e809e2c6d9961e" } wheels = [ { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/unreachable_wheels_a-1.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:e9fb30c5eb114114f9031d0ad2238614c2dcce203c5992848305ccda8f38a53e" }, @@ -5250,7 +5250,7 @@ fn unreachable_wheels() -> Result<()> { [[package]] name = "package-b" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } sdist = { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/unreachable_wheels_b-1.0.0.tar.gz", hash = "sha256:253ae69b963651cd5ac16601a445e2e179db9eac552e8cfc37aadf73a88931ed" } wheels = [ { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/unreachable_wheels_b-1.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3de2212ca86f1137324965899ce7f48640ed8db94578f4078d641520b77e13e" }, @@ -5260,7 +5260,7 @@ fn unreachable_wheels() -> Result<()> { [[package]] name = "package-c" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } sdist = { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/unreachable_wheels_c-1.0.0.tar.gz", hash = "sha256:5c4783e85f0fa57b720fd02b5c7e0ff8bc98121546fe2cce435710efe4a34b28" } wheels = [ { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/unreachable_wheels_c-1.0.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:4b846c5b1646b04828a2bef6c9d180ff7cfd725866013dcec8933de7fb5f9e8d" }, @@ -5362,7 +5362,7 @@ fn marker_variants_have_different_extras() -> Result<()> { [[package]] name = "package-psycopg" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } dependencies = [ { name = "package-tzdata", marker = "sys_platform == 'win32'" }, ] @@ -5379,7 +5379,7 @@ fn marker_variants_have_different_extras() -> Result<()> { [[package]] name = "package-psycopg-binary" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } sdist = { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/marker_variants_have_different_extras_psycopg_binary-1.0.0.tar.gz", hash = "sha256:9939771dfe78d76e3583492aaec576719780f744b36198b1f18bb16bb5048995" } wheels = [ { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/marker_variants_have_different_extras_psycopg_binary-1.0.0-py3-none-any.whl", hash = "sha256:4fb0aef60e76bc7e339d60dc919f3b6e27e49184ffdef9fb2c3f6902b23b6bd2" }, @@ -5388,7 +5388,7 @@ fn marker_variants_have_different_extras() -> Result<()> { [[package]] name = "package-tzdata" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } sdist = { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/marker_variants_have_different_extras_tzdata-1.0.0.tar.gz", hash = "sha256:5aa31d0aec969afbc13584c3209ca2380107bdab68578f881eb2da543ac2ee8e" } wheels = [ { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/marker_variants_have_different_extras_tzdata-1.0.0-py3-none-any.whl", hash = "sha256:7466eec7ed202434492e7c09a4a7327517aec6d549aaca0436dcc100f9fcb6a5" }, @@ -5515,7 +5515,7 @@ fn virtual_package_extra_priorities() -> Result<()> { [[package]] name = "package-a" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } dependencies = [ { name = "package-b" }, ] @@ -5527,7 +5527,7 @@ fn virtual_package_extra_priorities() -> Result<()> { [[package]] name = "package-b" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } sdist = { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/virtual_package_extra_priorities_b-1.0.0.tar.gz", hash = "sha256:79a54df14eb28687678447f5270f578f73b325f8234e620d375a87708fd7345c" } wheels = [ { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/virtual_package_extra_priorities_b-1.0.0-py3-none-any.whl", hash = "sha256:2aab1a3b90f215cb55b9bfde55b3c3617225ca0da726e8c9543c0727734f1df9" }, @@ -5635,7 +5635,7 @@ fn specific_architecture() -> Result<()> { [[package]] name = "package-a" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } dependencies = [ { name = "package-b", marker = "platform_machine == 'x86_64'" }, { name = "package-c", marker = "platform_machine == 'aarch64'" }, @@ -5649,7 +5649,7 @@ fn specific_architecture() -> Result<()> { [[package]] name = "package-b" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } wheels = [ { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/specific_architecture_b-1.0.0-cp313-cp313-freebsd_13_aarch64.whl", hash = "sha256:4ce70a68440d4aaa31cc1c6174b83b741e9b8f3074ad0f3ef41c572795378999" }, { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/specific_architecture_b-1.0.0-cp313-cp313-freebsd_13_x86_64.whl", hash = "sha256:4ce70a68440d4aaa31cc1c6174b83b741e9b8f3074ad0f3ef41c572795378999" }, @@ -5660,7 +5660,7 @@ fn specific_architecture() -> Result<()> { [[package]] name = "package-c" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } wheels = [ { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/specific_architecture_c-1.0.0-cp313-cp313-freebsd_13_aarch64.whl", hash = "sha256:b028c88fe496724cea4a7d95eb789a000b7f000067f95c922b09461be2746a3d" }, { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/specific_architecture_c-1.0.0-cp313-cp313-freebsd_13_x86_64.whl", hash = "sha256:b028c88fe496724cea4a7d95eb789a000b7f000067f95c922b09461be2746a3d" }, @@ -5671,7 +5671,7 @@ fn specific_architecture() -> Result<()> { [[package]] name = "package-d" version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" } wheels = [ { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/specific_architecture_d-1.0.0-cp313-cp313-freebsd_13_aarch64.whl", hash = "sha256:842864c1348694fab33199eb05921602c2abfc77844a81085a55db02edd30da4" }, { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/specific_architecture_d-1.0.0-cp313-cp313-freebsd_13_x86_64.whl", hash = "sha256:842864c1348694fab33199eb05921602c2abfc77844a81085a55db02edd30da4" }, diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index 4187de957..da59682ab 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -9953,7 +9953,7 @@ fn sync_required_environment_hint() -> Result<()> { ----- stderr ----- Resolved 2 packages in [TIME] - error: Distribution `no-sdist-no-wheels-with-matching-platform-a==1.0.0 @ registry+https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/` can't be installed because it doesn't have a source distribution or wheel for the current platform + error: Distribution `no-sdist-no-wheels-with-matching-platform-a==1.0.0 @ registry+https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html` can't be installed because it doesn't have a source distribution or wheel for the current platform hint: You're on [PLATFORM] (`[TAG]`), but `no-sdist-no-wheels-with-matching-platform-a` (v1.0.0) only has wheels for the following platform: `macosx_10_0_ppc64`; consider adding your platform to `tool.uv.required-environments` to ensure uv resolves to a version with compatible wheels "); From 880c5e4949725fa902e0b6ff117456a08e0409de Mon Sep 17 00:00:00 2001 From: John Mumm Date: Fri, 27 Jun 2025 19:26:28 +0200 Subject: [PATCH 105/185] Ensure preview default Python installs are upgradeable (#14261) Python `bin` installations installed with `uv python install --default --preview` (no version specified) were not being installed as upgradeable. Instead each link was pointed at the highest patch version for a minor version. This change ensures that these preview default installations are also treated as upgradeable. The PR includes some updates to the related tests. First, it checks the default install without specified version case. Second, since it's adding more read link checks, it creates a new `read_link` helper method to consolidate repeated logic and replace instances of `#[cfg(unix/windows)` with `if cfg!(unix/windows)`. Fixes #14247 --- crates/uv/src/commands/python/install.rs | 2 +- crates/uv/tests/it/python_install.rs | 209 ++++++++++++++++------- 2 files changed, 144 insertions(+), 67 deletions(-) diff --git a/crates/uv/src/commands/python/install.rs b/crates/uv/src/commands/python/install.rs index 7ad96fffe..08f95003e 100644 --- a/crates/uv/src/commands/python/install.rs +++ b/crates/uv/src/commands/python/install.rs @@ -451,7 +451,7 @@ pub(crate) async fn install( .expect("We should have a bin directory with preview enabled") .as_path(); - let upgradeable = preview.is_enabled() && is_default_install + let upgradeable = (default || is_default_install) || requested_minor_versions.contains(&installation.key().version().python_version()); create_bin_links( diff --git a/crates/uv/tests/it/python_install.rs b/crates/uv/tests/it/python_install.rs index 2b6f03d4b..817e71052 100644 --- a/crates/uv/tests/it/python_install.rs +++ b/crates/uv/tests/it/python_install.rs @@ -356,27 +356,20 @@ fn python_install_preview() { bin_python.assert(predicate::path::is_symlink()); // The link should be to a path containing a minor version symlink directory - #[cfg(unix)] - { + if cfg!(unix) { insta::with_settings!({ filters => context.filters(), }, { insta::assert_snapshot!( - &bin_python - .read_link() - .unwrap_or_else(|_| panic!("{} should be readable", bin_python.path().display())) - .as_os_str().to_string_lossy(), - @"[TEMP_DIR]/managed/cpython-3.13-[PLATFORM]/bin/python3.13" + read_link(&bin_python), @"[TEMP_DIR]/managed/cpython-3.13-[PLATFORM]/bin/python3.13" ); }); - } - #[cfg(windows)] - { + } else if cfg!(windows) { insta::with_settings!({ filters => context.filters(), }, { insta::assert_snapshot!( - launcher_path(&bin_python).display(), @"[TEMP_DIR]/managed/cpython-3.13-[PLATFORM]/python" + read_link(&bin_python), @"[TEMP_DIR]/managed/cpython-3.13-[PLATFORM]/python" ); }); } @@ -505,27 +498,20 @@ fn python_install_preview() { .child(format!("python3.11{}", std::env::consts::EXE_SUFFIX)); // The link should be to a path containing a minor version symlink directory - #[cfg(unix)] - { + if cfg!(unix) { insta::with_settings!({ filters => context.filters(), }, { insta::assert_snapshot!( - &bin_python - .read_link() - .unwrap_or_else(|_| panic!("{} should be readable", bin_python.path().display())) - .as_os_str().to_string_lossy(), - @"[TEMP_DIR]/managed/cpython-3.11-[PLATFORM]/bin/python3.11" + read_link(&bin_python), @"[TEMP_DIR]/managed/cpython-3.11-[PLATFORM]/bin/python3.11" ); }); - } - #[cfg(windows)] - { + } else if cfg!(windows) { insta::with_settings!({ filters => context.filters(), }, { insta::assert_snapshot!( - launcher_path(&bin_python).display(), @"[TEMP_DIR]/managed/cpython-3.11-[PLATFORM]/python" + read_link(&bin_python), @"[TEMP_DIR]/managed/cpython-3.11-[PLATFORM]/python" ); }); } @@ -563,15 +549,15 @@ fn python_install_preview() { filters => context.filters(), }, { insta::assert_snapshot!( - read_link_path(&bin_python), @"[TEMP_DIR]/managed/cpython-3.12.8-[PLATFORM]/bin/python3.12" + canonicalize_link_path(&bin_python), @"[TEMP_DIR]/managed/cpython-3.12.8-[PLATFORM]/bin/python3.12" ); }); - } else { + } else if cfg!(windows) { insta::with_settings!({ filters => context.filters(), }, { insta::assert_snapshot!( - read_link_path(&bin_python), @"[TEMP_DIR]/managed/cpython-3.12.8-[PLATFORM]/python" + canonicalize_link_path(&bin_python), @"[TEMP_DIR]/managed/cpython-3.12.8-[PLATFORM]/python" ); }); } @@ -600,27 +586,20 @@ fn python_install_preview_upgrade() { "###); // Installing with a patch version should cause the link to be to the patch installation. - #[cfg(unix)] - { + if cfg!(unix) { insta::with_settings!({ filters => context.filters(), }, { insta::assert_snapshot!( - &bin_python - .read_link() - .unwrap_or_else(|_| panic!("{} should be readable", bin_python.display())) - .display(), - @"[TEMP_DIR]/managed/cpython-3.12.5-[PLATFORM]/bin/python3.12" + read_link(&bin_python), @"[TEMP_DIR]/managed/cpython-3.12.5-[PLATFORM]/bin/python3.12" ); }); - } - #[cfg(windows)] - { + } else if cfg!(windows) { insta::with_settings!({ filters => context.filters(), }, { insta::assert_snapshot!( - launcher_path(&bin_python).display(), @"[TEMP_DIR]/managed/cpython-3.12.5-[PLATFORM]/python" + read_link(&bin_python), @"[TEMP_DIR]/managed/cpython-3.12.5-[PLATFORM]/python" ); }); } @@ -641,15 +620,15 @@ fn python_install_preview_upgrade() { filters => context.filters(), }, { insta::assert_snapshot!( - read_link_path(&bin_python), @"[TEMP_DIR]/managed/cpython-3.12.5-[PLATFORM]/bin/python3.12" + canonicalize_link_path(&bin_python), @"[TEMP_DIR]/managed/cpython-3.12.5-[PLATFORM]/bin/python3.12" ); }); - } else { + } else if cfg!(windows) { insta::with_settings!({ filters => context.filters(), }, { insta::assert_snapshot!( - read_link_path(&bin_python), @"[TEMP_DIR]/managed/cpython-3.12.5-[PLATFORM]/python" + canonicalize_link_path(&bin_python), @"[TEMP_DIR]/managed/cpython-3.12.5-[PLATFORM]/python" ); }); } @@ -670,15 +649,15 @@ fn python_install_preview_upgrade() { filters => context.filters(), }, { insta::assert_snapshot!( - read_link_path(&bin_python), @"[TEMP_DIR]/managed/cpython-3.12.5-[PLATFORM]/bin/python3.12" + canonicalize_link_path(&bin_python), @"[TEMP_DIR]/managed/cpython-3.12.5-[PLATFORM]/bin/python3.12" ); }); - } else { + } else if cfg!(windows) { insta::with_settings!({ filters => context.filters(), }, { insta::assert_snapshot!( - read_link_path(&bin_python), @"[TEMP_DIR]/managed/cpython-3.12.5-[PLATFORM]/python" + canonicalize_link_path(&bin_python), @"[TEMP_DIR]/managed/cpython-3.12.5-[PLATFORM]/python" ); }); } @@ -699,15 +678,15 @@ fn python_install_preview_upgrade() { filters => context.filters(), }, { insta::assert_snapshot!( - read_link_path(&bin_python), @"[TEMP_DIR]/managed/cpython-3.12.4-[PLATFORM]/bin/python3.12" + canonicalize_link_path(&bin_python), @"[TEMP_DIR]/managed/cpython-3.12.4-[PLATFORM]/bin/python3.12" ); }); - } else { + } else if cfg!(windows) { insta::with_settings!({ filters => context.filters(), }, { insta::assert_snapshot!( - read_link_path(&bin_python), @"[TEMP_DIR]/managed/cpython-3.12.4-[PLATFORM]/python" + canonicalize_link_path(&bin_python), @"[TEMP_DIR]/managed/cpython-3.12.4-[PLATFORM]/python" ); }); } @@ -728,15 +707,15 @@ fn python_install_preview_upgrade() { filters => context.filters(), }, { insta::assert_snapshot!( - read_link_path(&bin_python), @"[TEMP_DIR]/managed/cpython-3.12.6-[PLATFORM]/bin/python3.12" + canonicalize_link_path(&bin_python), @"[TEMP_DIR]/managed/cpython-3.12.6-[PLATFORM]/bin/python3.12" ); }); - } else { + } else if cfg!(windows) { insta::with_settings!({ filters => context.filters(), }, { insta::assert_snapshot!( - read_link_path(&bin_python), @"[TEMP_DIR]/managed/cpython-3.12.6-[PLATFORM]/python" + canonicalize_link_path(&bin_python), @"[TEMP_DIR]/managed/cpython-3.12.6-[PLATFORM]/python" ); }); } @@ -933,7 +912,7 @@ fn python_install_default() { bin_python_default.assert(predicate::path::missing()); // Install the latest version, i.e., a "default install" - uv_snapshot!(context.filters(), context.python_install().arg("--preview"), @r" + uv_snapshot!(context.filters(), context.python_install().arg("--default").arg("--preview"), @r" success: true exit_code: 0 ----- stdout ----- @@ -948,6 +927,75 @@ fn python_install_default() { bin_python_major.assert(predicate::path::exists()); bin_python_default.assert(predicate::path::exists()); + // And 3.13 should be the default + if cfg!(unix) { + insta::with_settings!({ + filters => context.filters(), + }, { + insta::assert_snapshot!( + read_link(&bin_python_major), @"[TEMP_DIR]/managed/cpython-3.13-[PLATFORM]/bin/python3.13" + ); + insta::assert_snapshot!( + canonicalize_link_path(&bin_python_major), @"[TEMP_DIR]/managed/cpython-3.13.5-[PLATFORM]/bin/python3.13" + ); + }); + + insta::with_settings!({ + filters => context.filters(), + }, { + insta::assert_snapshot!( + read_link(&bin_python_minor_13), @"[TEMP_DIR]/managed/cpython-3.13-[PLATFORM]/bin/python3.13" + ); + insta::assert_snapshot!( + canonicalize_link_path(&bin_python_minor_13), @"[TEMP_DIR]/managed/cpython-3.13.5-[PLATFORM]/bin/python3.13" + ); + }); + + insta::with_settings!({ + filters => context.filters(), + }, { + insta::assert_snapshot!( + read_link(&bin_python_default), @"[TEMP_DIR]/managed/cpython-3.13-[PLATFORM]/bin/python3.13" + ); + insta::assert_snapshot!( + canonicalize_link_path(&bin_python_default), @"[TEMP_DIR]/managed/cpython-3.13.5-[PLATFORM]/bin/python3.13" + ); + }); + } else if cfg!(windows) { + insta::with_settings!({ + filters => context.filters(), + }, { + insta::assert_snapshot!( + read_link(&bin_python_major), @"[TEMP_DIR]/managed/cpython-3.13-[PLATFORM]/python" + ); + insta::assert_snapshot!( + canonicalize_link_path(&bin_python_major), @"[TEMP_DIR]/managed/cpython-3.13.5-[PLATFORM]/python" + ); + }); + + insta::with_settings!({ + filters => context.filters(), + }, { + insta::assert_snapshot!( + read_link(&bin_python_minor_13), @"[TEMP_DIR]/managed/cpython-3.13-[PLATFORM]/python" + ); + insta::assert_snapshot!( + canonicalize_link_path(&bin_python_minor_13), @"[TEMP_DIR]/managed/cpython-3.13.5-[PLATFORM]/python" + ); + }); + + insta::with_settings!({ + filters => context.filters(), + }, { + insta::assert_snapshot!( + read_link(&bin_python_default), @"[TEMP_DIR]/managed/cpython-3.13-[PLATFORM]/python" + ); + insta::assert_snapshot!( + canonicalize_link_path(&bin_python_default), @"[TEMP_DIR]/managed/cpython-3.13.5-[PLATFORM]/python" + ); + }); + } + // Uninstall again uv_snapshot!(context.filters(), context.python_uninstall().arg("3.13"), @r" success: true @@ -1001,7 +1049,10 @@ fn python_install_default() { filters => context.filters(), }, { insta::assert_snapshot!( - read_link_path(&bin_python_major), @"[TEMP_DIR]/managed/cpython-3.12.11-[PLATFORM]/bin/python3.12" + read_link(&bin_python_major), @"[TEMP_DIR]/managed/cpython-3.12-[PLATFORM]/bin/python3.12" + ); + insta::assert_snapshot!( + canonicalize_link_path(&bin_python_major), @"[TEMP_DIR]/managed/cpython-3.12.11-[PLATFORM]/bin/python3.12" ); }); @@ -1009,7 +1060,10 @@ fn python_install_default() { filters => context.filters(), }, { insta::assert_snapshot!( - read_link_path(&bin_python_minor_12), @"[TEMP_DIR]/managed/cpython-3.12.11-[PLATFORM]/bin/python3.12" + read_link(&bin_python_minor_12), @"[TEMP_DIR]/managed/cpython-3.12-[PLATFORM]/bin/python3.12" + ); + insta::assert_snapshot!( + canonicalize_link_path(&bin_python_minor_12), @"[TEMP_DIR]/managed/cpython-3.12.11-[PLATFORM]/bin/python3.12" ); }); @@ -1017,7 +1071,10 @@ fn python_install_default() { filters => context.filters(), }, { insta::assert_snapshot!( - read_link_path(&bin_python_default), @"[TEMP_DIR]/managed/cpython-3.12.11-[PLATFORM]/bin/python3.12" + read_link(&bin_python_default), @"[TEMP_DIR]/managed/cpython-3.12-[PLATFORM]/bin/python3.12" + ); + insta::assert_snapshot!( + canonicalize_link_path(&bin_python_default), @"[TEMP_DIR]/managed/cpython-3.12.11-[PLATFORM]/bin/python3.12" ); }); } else { @@ -1025,7 +1082,10 @@ fn python_install_default() { filters => context.filters(), }, { insta::assert_snapshot!( - read_link_path(&bin_python_major), @"[TEMP_DIR]/managed/cpython-3.12.11-[PLATFORM]/python" + read_link(&bin_python_major), @"[TEMP_DIR]/managed/cpython-3.12-[PLATFORM]/python" + ); + insta::assert_snapshot!( + canonicalize_link_path(&bin_python_major), @"[TEMP_DIR]/managed/cpython-3.12.11-[PLATFORM]/python" ); }); @@ -1033,7 +1093,10 @@ fn python_install_default() { filters => context.filters(), }, { insta::assert_snapshot!( - read_link_path(&bin_python_minor_12), @"[TEMP_DIR]/managed/cpython-3.12.11-[PLATFORM]/python" + read_link(&bin_python_minor_12), @"[TEMP_DIR]/managed/cpython-3.12-[PLATFORM]/python" + ); + insta::assert_snapshot!( + canonicalize_link_path(&bin_python_minor_12), @"[TEMP_DIR]/managed/cpython-3.12.11-[PLATFORM]/python" ); }); @@ -1041,7 +1104,10 @@ fn python_install_default() { filters => context.filters(), }, { insta::assert_snapshot!( - read_link_path(&bin_python_default), @"[TEMP_DIR]/managed/cpython-3.12.11-[PLATFORM]/python" + read_link(&bin_python_default), @"[TEMP_DIR]/managed/cpython-3.12-[PLATFORM]/python" + ); + insta::assert_snapshot!( + canonicalize_link_path(&bin_python_default), @"[TEMP_DIR]/managed/cpython-3.12.11-[PLATFORM]/python" ); }); } @@ -1069,7 +1135,7 @@ fn python_install_default() { filters => context.filters(), }, { insta::assert_snapshot!( - read_link_path(&bin_python_major), @"[TEMP_DIR]/managed/cpython-3.13.5-[PLATFORM]/bin/python3.13" + canonicalize_link_path(&bin_python_major), @"[TEMP_DIR]/managed/cpython-3.13.5-[PLATFORM]/bin/python3.13" ); }); @@ -1077,7 +1143,7 @@ fn python_install_default() { filters => context.filters(), }, { insta::assert_snapshot!( - read_link_path(&bin_python_minor_13), @"[TEMP_DIR]/managed/cpython-3.13.5-[PLATFORM]/bin/python3.13" + canonicalize_link_path(&bin_python_minor_13), @"[TEMP_DIR]/managed/cpython-3.13.5-[PLATFORM]/bin/python3.13" ); }); @@ -1085,7 +1151,7 @@ fn python_install_default() { filters => context.filters(), }, { insta::assert_snapshot!( - read_link_path(&bin_python_minor_12), @"[TEMP_DIR]/managed/cpython-3.12.11-[PLATFORM]/bin/python3.12" + canonicalize_link_path(&bin_python_minor_12), @"[TEMP_DIR]/managed/cpython-3.12.11-[PLATFORM]/bin/python3.12" ); }); @@ -1093,15 +1159,15 @@ fn python_install_default() { filters => context.filters(), }, { insta::assert_snapshot!( - read_link_path(&bin_python_default), @"[TEMP_DIR]/managed/cpython-3.13.5-[PLATFORM]/bin/python3.13" + canonicalize_link_path(&bin_python_default), @"[TEMP_DIR]/managed/cpython-3.13.5-[PLATFORM]/bin/python3.13" ); }); - } else { + } else if cfg!(windows) { insta::with_settings!({ filters => context.filters(), }, { insta::assert_snapshot!( - read_link_path(&bin_python_major), @"[TEMP_DIR]/managed/cpython-3.13.5-[PLATFORM]/python" + canonicalize_link_path(&bin_python_major), @"[TEMP_DIR]/managed/cpython-3.13.5-[PLATFORM]/python" ); }); @@ -1109,7 +1175,7 @@ fn python_install_default() { filters => context.filters(), }, { insta::assert_snapshot!( - read_link_path(&bin_python_minor_13), @"[TEMP_DIR]/managed/cpython-3.13.5-[PLATFORM]/python" + canonicalize_link_path(&bin_python_minor_13), @"[TEMP_DIR]/managed/cpython-3.13.5-[PLATFORM]/python" ); }); @@ -1117,7 +1183,7 @@ fn python_install_default() { filters => context.filters(), }, { insta::assert_snapshot!( - read_link_path(&bin_python_minor_12), @"[TEMP_DIR]/managed/cpython-3.12.11-[PLATFORM]/python" + canonicalize_link_path(&bin_python_minor_12), @"[TEMP_DIR]/managed/cpython-3.12.11-[PLATFORM]/python" ); }); @@ -1125,7 +1191,7 @@ fn python_install_default() { filters => context.filters(), }, { insta::assert_snapshot!( - read_link_path(&bin_python_default), @"[TEMP_DIR]/managed/cpython-3.13.5-[PLATFORM]/python" + canonicalize_link_path(&bin_python_default), @"[TEMP_DIR]/managed/cpython-3.13.5-[PLATFORM]/python" ); }); } @@ -1139,7 +1205,7 @@ fn launcher_path(path: &Path) -> PathBuf { launcher.python_path } -fn read_link_path(path: &Path) -> String { +fn canonicalize_link_path(path: &Path) -> String { #[cfg(unix)] let canonical_path = fs_err::canonicalize(path); @@ -1152,6 +1218,17 @@ fn read_link_path(path: &Path) -> String { .to_string() } +fn read_link(path: &Path) -> String { + #[cfg(unix)] + let linked_path = + fs_err::read_link(path).unwrap_or_else(|_| panic!("{} should be readable", path.display())); + + #[cfg(windows)] + let linked_path = launcher_path(path); + + linked_path.simplified_display().to_string() +} + #[test] fn python_install_unknown() { let context: TestContext = TestContext::new_with_versions(&[]).with_managed_python_dirs(); @@ -1212,7 +1289,7 @@ fn python_install_preview_broken_link() { filters => context.filters(), }, { insta::assert_snapshot!( - read_link_path(&bin_python), @"[TEMP_DIR]/managed/cpython-3.13.5-[PLATFORM]/bin/python3.13" + canonicalize_link_path(&bin_python), @"[TEMP_DIR]/managed/cpython-3.13.5-[PLATFORM]/bin/python3.13" ); }); } From 74468dac15e9d357cc68cca9a32585fd862208b9 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Fri, 27 Jun 2025 12:36:07 -0500 Subject: [PATCH 106/185] Bump `python-build-standalone` releases to include 3.14.0b3 (#14301) See https://github.com/astral-sh/python-build-standalone/releases/tag/20250626 --- crates/uv-python/download-metadata.json | 1512 +++++++++++++++++------ crates/uv/tests/it/python_install.rs | 10 +- 2 files changed, 1161 insertions(+), 361 deletions(-) diff --git a/crates/uv-python/download-metadata.json b/crates/uv-python/download-metadata.json index 71fe83c78..367efbd64 100644 --- a/crates/uv-python/download-metadata.json +++ b/crates/uv-python/download-metadata.json @@ -1,4 +1,804 @@ { + "cpython-3.14.0b3-darwin-aarch64-none": { + "name": "cpython", + "arch": { + "family": "aarch64", + "variant": null + }, + "os": "darwin", + "libc": "none", + "major": 3, + "minor": 14, + "patch": 0, + "prerelease": "b3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-aarch64-apple-darwin-install_only_stripped.tar.gz", + "sha256": "93e38ced4feb2fe93d342d75918a1fb107949ce8378b6cb16c0fc78ab0c24d20", + "variant": null + }, + "cpython-3.14.0b3-darwin-x86_64-none": { + "name": "cpython", + "arch": { + "family": "x86_64", + "variant": null + }, + "os": "darwin", + "libc": "none", + "major": 3, + "minor": 14, + "patch": 0, + "prerelease": "b3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-x86_64-apple-darwin-install_only_stripped.tar.gz", + "sha256": "ea08efdd3b518139ed8b42aa5416e96e01b254aa78b409c6701163f2210fb37b", + "variant": null + }, + "cpython-3.14.0b3-linux-aarch64-gnu": { + "name": "cpython", + "arch": { + "family": "aarch64", + "variant": null + }, + "os": "linux", + "libc": "gnu", + "major": 3, + "minor": 14, + "patch": 0, + "prerelease": "b3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "3444de5a35a41a162df647eaec4d758eca966c4243ee22d0d1f8597edf48e5b8", + "variant": null + }, + "cpython-3.14.0b3-linux-armv7-gnueabi": { + "name": "cpython", + "arch": { + "family": "armv7", + "variant": null + }, + "os": "linux", + "libc": "gnueabi", + "major": 3, + "minor": 14, + "patch": 0, + "prerelease": "b3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-armv7-unknown-linux-gnueabi-install_only_stripped.tar.gz", + "sha256": "ac1ce5cd8aa09f6584636e3bda2a14a8df46aaf435b73511cc0e57e237d77211", + "variant": null + }, + "cpython-3.14.0b3-linux-armv7-gnueabihf": { + "name": "cpython", + "arch": { + "family": "armv7", + "variant": null + }, + "os": "linux", + "libc": "gnueabihf", + "major": 3, + "minor": 14, + "patch": 0, + "prerelease": "b3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-armv7-unknown-linux-gnueabihf-install_only_stripped.tar.gz", + "sha256": "9058c9d70434693f5d6fa052c28dc7d7850e77efee6d32495d9d2605421b656d", + "variant": null + }, + "cpython-3.14.0b3-linux-powerpc64le-gnu": { + "name": "cpython", + "arch": { + "family": "powerpc64le", + "variant": null + }, + "os": "linux", + "libc": "gnu", + "major": 3, + "minor": 14, + "patch": 0, + "prerelease": "b3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-ppc64le-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "ad7e0d08638de2af6c70c13990b65d1bf412c95c28063de3252b186b48e5f29f", + "variant": null + }, + "cpython-3.14.0b3-linux-riscv64-gnu": { + "name": "cpython", + "arch": { + "family": "riscv64", + "variant": null + }, + "os": "linux", + "libc": "gnu", + "major": 3, + "minor": 14, + "patch": 0, + "prerelease": "b3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-riscv64-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "e95f10b21b149dc8fa59198d4cc0ff3720ec814aed9d03f33932e31cf8c17bf8", + "variant": null + }, + "cpython-3.14.0b3-linux-s390x-gnu": { + "name": "cpython", + "arch": { + "family": "s390x", + "variant": null + }, + "os": "linux", + "libc": "gnu", + "major": 3, + "minor": 14, + "patch": 0, + "prerelease": "b3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-s390x-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "7887516bf789785ce5cb7fa2eb2b4cffc1c454efe89a8eacfa6c66b2e054c9f8", + "variant": null + }, + "cpython-3.14.0b3-linux-x86_64-gnu": { + "name": "cpython", + "arch": { + "family": "x86_64", + "variant": null + }, + "os": "linux", + "libc": "gnu", + "major": 3, + "minor": 14, + "patch": 0, + "prerelease": "b3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "014a2e7c96957cc3ac3925b8b1c06e6068cab3b20faf5f9f329f7e8d95d41bfd", + "variant": null + }, + "cpython-3.14.0b3-linux-x86_64-musl": { + "name": "cpython", + "arch": { + "family": "x86_64", + "variant": null + }, + "os": "linux", + "libc": "musl", + "major": 3, + "minor": 14, + "patch": 0, + "prerelease": "b3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-x86_64-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "72e15bb93e93cc4abcdd8ed44a666d12267ecd048d6e3b1bfeda3cc08187e659", + "variant": null + }, + "cpython-3.14.0b3-linux-x86_64_v2-gnu": { + "name": "cpython", + "arch": { + "family": "x86_64", + "variant": "v2" + }, + "os": "linux", + "libc": "gnu", + "major": 3, + "minor": 14, + "patch": 0, + "prerelease": "b3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-x86_64_v2-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "fff629607727d50f04deca03056259f9aee607969fb2bf7db969587e53853302", + "variant": null + }, + "cpython-3.14.0b3-linux-x86_64_v2-musl": { + "name": "cpython", + "arch": { + "family": "x86_64", + "variant": "v2" + }, + "os": "linux", + "libc": "musl", + "major": 3, + "minor": 14, + "patch": 0, + "prerelease": "b3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-x86_64_v2-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "acb6aeebf0c6bc6903600ce99d112754cc830a89224d0d63ef3c5c107f01685c", + "variant": null + }, + "cpython-3.14.0b3-linux-x86_64_v3-gnu": { + "name": "cpython", + "arch": { + "family": "x86_64", + "variant": "v3" + }, + "os": "linux", + "libc": "gnu", + "major": 3, + "minor": 14, + "patch": 0, + "prerelease": "b3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-x86_64_v3-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "df49f1a8e5c5aefc28ebc169fff77047878d0ae7fb7bf00221e10fc59f67f5fc", + "variant": null + }, + "cpython-3.14.0b3-linux-x86_64_v3-musl": { + "name": "cpython", + "arch": { + "family": "x86_64", + "variant": "v3" + }, + "os": "linux", + "libc": "musl", + "major": 3, + "minor": 14, + "patch": 0, + "prerelease": "b3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-x86_64_v3-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "d4672c060c5d8963af6355daaca40bb53938eee47a7346cab0259abff8e4f724", + "variant": null + }, + "cpython-3.14.0b3-linux-x86_64_v4-gnu": { + "name": "cpython", + "arch": { + "family": "x86_64", + "variant": "v4" + }, + "os": "linux", + "libc": "gnu", + "major": 3, + "minor": 14, + "patch": 0, + "prerelease": "b3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-x86_64_v4-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "925a47c3385ed2e2d498cd8f7a0a9434242b4b42d80ba45149d35da34bc9953b", + "variant": null + }, + "cpython-3.14.0b3-linux-x86_64_v4-musl": { + "name": "cpython", + "arch": { + "family": "x86_64", + "variant": "v4" + }, + "os": "linux", + "libc": "musl", + "major": 3, + "minor": 14, + "patch": 0, + "prerelease": "b3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-x86_64_v4-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "3e3b57bd9b3e821bc26107afaa29457ef8e96a50c8254ca89796feb9386928a9", + "variant": null + }, + "cpython-3.14.0b3-windows-i686-none": { + "name": "cpython", + "arch": { + "family": "i686", + "variant": null + }, + "os": "windows", + "libc": "none", + "major": 3, + "minor": 14, + "patch": 0, + "prerelease": "b3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-i686-pc-windows-msvc-install_only_stripped.tar.gz", + "sha256": "9009cd7a90b1ab3e7325278fbaa5363f1c160c92edef05be5c9d0a5c67ede59e", + "variant": null + }, + "cpython-3.14.0b3-windows-x86_64-none": { + "name": "cpython", + "arch": { + "family": "x86_64", + "variant": null + }, + "os": "windows", + "libc": "none", + "major": 3, + "minor": 14, + "patch": 0, + "prerelease": "b3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-x86_64-pc-windows-msvc-install_only_stripped.tar.gz", + "sha256": "b50dd04ffb2fdc051964cc733ed624c5ea7cae85ec51060b1b97a406dd00c176", + "variant": null + }, + "cpython-3.14.0b3+freethreaded-darwin-aarch64-none": { + "name": "cpython", + "arch": { + "family": "aarch64", + "variant": null + }, + "os": "darwin", + "libc": "none", + "major": 3, + "minor": 14, + "patch": 0, + "prerelease": "b3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-aarch64-apple-darwin-freethreaded%2Bpgo%2Blto-full.tar.zst", + "sha256": "0ad27d76b4a5ebe3fac67bf928ea07bea5335fe6f0f33880277db73640a96df1", + "variant": "freethreaded" + }, + "cpython-3.14.0b3+freethreaded-darwin-x86_64-none": { + "name": "cpython", + "arch": { + "family": "x86_64", + "variant": null + }, + "os": "darwin", + "libc": "none", + "major": 3, + "minor": 14, + "patch": 0, + "prerelease": "b3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-x86_64-apple-darwin-freethreaded%2Bpgo%2Blto-full.tar.zst", + "sha256": "26e5c3e51de17455ed4c7f2b81702b175cf230728e4fdd93b3c426d21df09df2", + "variant": "freethreaded" + }, + "cpython-3.14.0b3+freethreaded-linux-aarch64-gnu": { + "name": "cpython", + "arch": { + "family": "aarch64", + "variant": null + }, + "os": "linux", + "libc": "gnu", + "major": 3, + "minor": 14, + "patch": 0, + "prerelease": "b3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-aarch64-unknown-linux-gnu-freethreaded%2Blto-full.tar.zst", + "sha256": "15abb894679fafa47e71b37fb722de526cad5a55b42998d9ba7201023299631b", + "variant": "freethreaded" + }, + "cpython-3.14.0b3+freethreaded-linux-armv7-gnueabi": { + "name": "cpython", + "arch": { + "family": "armv7", + "variant": null + }, + "os": "linux", + "libc": "gnueabi", + "major": 3, + "minor": 14, + "patch": 0, + "prerelease": "b3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-armv7-unknown-linux-gnueabi-freethreaded%2Blto-full.tar.zst", + "sha256": "a2e1f3628beec26b88eb5d5139fcc95a296d163a5a02910337a6a309450e340f", + "variant": "freethreaded" + }, + "cpython-3.14.0b3+freethreaded-linux-armv7-gnueabihf": { + "name": "cpython", + "arch": { + "family": "armv7", + "variant": null + }, + "os": "linux", + "libc": "gnueabihf", + "major": 3, + "minor": 14, + "patch": 0, + "prerelease": "b3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-armv7-unknown-linux-gnueabihf-freethreaded%2Blto-full.tar.zst", + "sha256": "30896f01c54a99e34d7d91a893568c32f99c597ecba8767ab83f501b770a3831", + "variant": "freethreaded" + }, + "cpython-3.14.0b3+freethreaded-linux-powerpc64le-gnu": { + "name": "cpython", + "arch": { + "family": "powerpc64le", + "variant": null + }, + "os": "linux", + "libc": "gnu", + "major": 3, + "minor": 14, + "patch": 0, + "prerelease": "b3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-ppc64le-unknown-linux-gnu-freethreaded%2Blto-full.tar.zst", + "sha256": "2127b36c4b16da76a713fb4c2e8a6a2757a6d73a07a6ee4fa14d2a02633e0605", + "variant": "freethreaded" + }, + "cpython-3.14.0b3+freethreaded-linux-riscv64-gnu": { + "name": "cpython", + "arch": { + "family": "riscv64", + "variant": null + }, + "os": "linux", + "libc": "gnu", + "major": 3, + "minor": 14, + "patch": 0, + "prerelease": "b3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-riscv64-unknown-linux-gnu-freethreaded%2Blto-full.tar.zst", + "sha256": "dca86f8df4d6a65a69e8deb65e60ed0b27a376f2d677ec9150138d4e3601f4f7", + "variant": "freethreaded" + }, + "cpython-3.14.0b3+freethreaded-linux-s390x-gnu": { + "name": "cpython", + "arch": { + "family": "s390x", + "variant": null + }, + "os": "linux", + "libc": "gnu", + "major": 3, + "minor": 14, + "patch": 0, + "prerelease": "b3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-s390x-unknown-linux-gnu-freethreaded%2Blto-full.tar.zst", + "sha256": "5f119f34846d6f150c8de3b8ce81418f5cf60f90b51fcc594cb54d6ab4db030d", + "variant": "freethreaded" + }, + "cpython-3.14.0b3+freethreaded-linux-x86_64-gnu": { + "name": "cpython", + "arch": { + "family": "x86_64", + "variant": null + }, + "os": "linux", + "libc": "gnu", + "major": 3, + "minor": 14, + "patch": 0, + "prerelease": "b3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-x86_64-unknown-linux-gnu-freethreaded%2Bpgo%2Blto-full.tar.zst", + "sha256": "ac5373d3b945298f34f1ebd5b03ce35ce92165638443ef65f8ca2d2eba07e39d", + "variant": "freethreaded" + }, + "cpython-3.14.0b3+freethreaded-linux-x86_64-musl": { + "name": "cpython", + "arch": { + "family": "x86_64", + "variant": null + }, + "os": "linux", + "libc": "musl", + "major": 3, + "minor": 14, + "patch": 0, + "prerelease": "b3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-x86_64-unknown-linux-musl-freethreaded%2Blto-full.tar.zst", + "sha256": "e08e0202777269916295cf570e78bfb160250f88c20bd6f27fd1a72dcb03c8b9", + "variant": "freethreaded" + }, + "cpython-3.14.0b3+freethreaded-linux-x86_64_v2-gnu": { + "name": "cpython", + "arch": { + "family": "x86_64", + "variant": "v2" + }, + "os": "linux", + "libc": "gnu", + "major": 3, + "minor": 14, + "patch": 0, + "prerelease": "b3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-x86_64_v2-unknown-linux-gnu-freethreaded%2Bpgo%2Blto-full.tar.zst", + "sha256": "5a36083ac0df9c72416a9cde665c6f86dfe78ebb348fc6a7b4b155ef0112fec9", + "variant": "freethreaded" + }, + "cpython-3.14.0b3+freethreaded-linux-x86_64_v2-musl": { + "name": "cpython", + "arch": { + "family": "x86_64", + "variant": "v2" + }, + "os": "linux", + "libc": "musl", + "major": 3, + "minor": 14, + "patch": 0, + "prerelease": "b3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-x86_64_v2-unknown-linux-musl-freethreaded%2Blto-full.tar.zst", + "sha256": "70297d1edad54eea78c8cd5174e21ab7e2ed2d754f512896ae0f4006517b101c", + "variant": "freethreaded" + }, + "cpython-3.14.0b3+freethreaded-linux-x86_64_v3-gnu": { + "name": "cpython", + "arch": { + "family": "x86_64", + "variant": "v3" + }, + "os": "linux", + "libc": "gnu", + "major": 3, + "minor": 14, + "patch": 0, + "prerelease": "b3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-x86_64_v3-unknown-linux-gnu-freethreaded%2Bpgo%2Blto-full.tar.zst", + "sha256": "8cce11843eae8c78f0045227f7637c451943406aa04c1dc693ca7bf4874b2cbd", + "variant": "freethreaded" + }, + "cpython-3.14.0b3+freethreaded-linux-x86_64_v3-musl": { + "name": "cpython", + "arch": { + "family": "x86_64", + "variant": "v3" + }, + "os": "linux", + "libc": "musl", + "major": 3, + "minor": 14, + "patch": 0, + "prerelease": "b3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-x86_64_v3-unknown-linux-musl-freethreaded%2Blto-full.tar.zst", + "sha256": "8da62b2823fb28d8d34093b20ac0d55034a47715afa35ed3a0fab49f2cc74e49", + "variant": "freethreaded" + }, + "cpython-3.14.0b3+freethreaded-linux-x86_64_v4-gnu": { + "name": "cpython", + "arch": { + "family": "x86_64", + "variant": "v4" + }, + "os": "linux", + "libc": "gnu", + "major": 3, + "minor": 14, + "patch": 0, + "prerelease": "b3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-x86_64_v4-unknown-linux-gnu-freethreaded%2Bpgo%2Blto-full.tar.zst", + "sha256": "ded53979b407e350ad4c9e87646f9521762c0769aa26d9450ba02ec6801961a2", + "variant": "freethreaded" + }, + "cpython-3.14.0b3+freethreaded-linux-x86_64_v4-musl": { + "name": "cpython", + "arch": { + "family": "x86_64", + "variant": "v4" + }, + "os": "linux", + "libc": "musl", + "major": 3, + "minor": 14, + "patch": 0, + "prerelease": "b3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-x86_64_v4-unknown-linux-musl-freethreaded%2Blto-full.tar.zst", + "sha256": "d90c08a393ee21fd9c4d3d7f4dcf5b14cb6251d3cb77df15cccc233e67fd85c1", + "variant": "freethreaded" + }, + "cpython-3.14.0b3+freethreaded-windows-i686-none": { + "name": "cpython", + "arch": { + "family": "i686", + "variant": null + }, + "os": "windows", + "libc": "none", + "major": 3, + "minor": 14, + "patch": 0, + "prerelease": "b3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-i686-pc-windows-msvc-freethreaded%2Bpgo-full.tar.zst", + "sha256": "97b59f1087c7f05c088a84998c0babf1b824358d28759e70c8090ec9a45e36aa", + "variant": "freethreaded" + }, + "cpython-3.14.0b3+freethreaded-windows-x86_64-none": { + "name": "cpython", + "arch": { + "family": "x86_64", + "variant": null + }, + "os": "windows", + "libc": "none", + "major": 3, + "minor": 14, + "patch": 0, + "prerelease": "b3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-x86_64-pc-windows-msvc-freethreaded%2Bpgo-full.tar.zst", + "sha256": "5c864084d8b8d5b5e9d4d5005f92ec2f7bdb65c13bc9b95a9ac52b2bcb4db8e0", + "variant": "freethreaded" + }, + "cpython-3.14.0b3+debug-linux-aarch64-gnu": { + "name": "cpython", + "arch": { + "family": "aarch64", + "variant": null + }, + "os": "linux", + "libc": "gnu", + "major": 3, + "minor": 14, + "patch": 0, + "prerelease": "b3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-aarch64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "6ad6bcadb2c1b862f3dd8c3a56c90eac2349a54dcf14628385d0d3bf5e993173", + "variant": "debug" + }, + "cpython-3.14.0b3+debug-linux-armv7-gnueabi": { + "name": "cpython", + "arch": { + "family": "armv7", + "variant": null + }, + "os": "linux", + "libc": "gnueabi", + "major": 3, + "minor": 14, + "patch": 0, + "prerelease": "b3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-armv7-unknown-linux-gnueabi-debug-full.tar.zst", + "sha256": "bd88ac4f905609bc5ee3ec6fc9d3482ce16f05b14052946aec3ed7f9ba8f83d2", + "variant": "debug" + }, + "cpython-3.14.0b3+debug-linux-armv7-gnueabihf": { + "name": "cpython", + "arch": { + "family": "armv7", + "variant": null + }, + "os": "linux", + "libc": "gnueabihf", + "major": 3, + "minor": 14, + "patch": 0, + "prerelease": "b3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-armv7-unknown-linux-gnueabihf-debug-full.tar.zst", + "sha256": "4d94559a5ae00bf849f5204a2f66eee119dd979cc0da8089edd1b57bce5a165f", + "variant": "debug" + }, + "cpython-3.14.0b3+debug-linux-powerpc64le-gnu": { + "name": "cpython", + "arch": { + "family": "powerpc64le", + "variant": null + }, + "os": "linux", + "libc": "gnu", + "major": 3, + "minor": 14, + "patch": 0, + "prerelease": "b3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-ppc64le-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "11a6c51fe6c0e9dfc9fdd7439151b34f5e4896f82f1894fd1aee32052e5ca4d2", + "variant": "debug" + }, + "cpython-3.14.0b3+debug-linux-riscv64-gnu": { + "name": "cpython", + "arch": { + "family": "riscv64", + "variant": null + }, + "os": "linux", + "libc": "gnu", + "major": 3, + "minor": 14, + "patch": 0, + "prerelease": "b3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-riscv64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "4e84e69007d047e1f6a76ea894a8b919e82e7f86860525eb3a42a9cb30ce7044", + "variant": "debug" + }, + "cpython-3.14.0b3+debug-linux-s390x-gnu": { + "name": "cpython", + "arch": { + "family": "s390x", + "variant": null + }, + "os": "linux", + "libc": "gnu", + "major": 3, + "minor": 14, + "patch": 0, + "prerelease": "b3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-s390x-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "f2f8dde10c119bbb3313a7ba4db59dd09124e283f2a65217f50504d18e9c511e", + "variant": "debug" + }, + "cpython-3.14.0b3+debug-linux-x86_64-gnu": { + "name": "cpython", + "arch": { + "family": "x86_64", + "variant": null + }, + "os": "linux", + "libc": "gnu", + "major": 3, + "minor": 14, + "patch": 0, + "prerelease": "b3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-x86_64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "26b5ad31f9902f271bc5e3e886975d242d4155ea43968e695214147b6f0758a3", + "variant": "debug" + }, + "cpython-3.14.0b3+debug-linux-x86_64-musl": { + "name": "cpython", + "arch": { + "family": "x86_64", + "variant": null + }, + "os": "linux", + "libc": "musl", + "major": 3, + "minor": 14, + "patch": 0, + "prerelease": "b3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-x86_64-unknown-linux-musl-debug-full.tar.zst", + "sha256": "6c608708569a018db38e888410c7871b00ed9b5caa3dabf19ddc51e64e2200ab", + "variant": "debug" + }, + "cpython-3.14.0b3+debug-linux-x86_64_v2-gnu": { + "name": "cpython", + "arch": { + "family": "x86_64", + "variant": "v2" + }, + "os": "linux", + "libc": "gnu", + "major": 3, + "minor": 14, + "patch": 0, + "prerelease": "b3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "2499487ea8a5aa28c3ae5e9073e9cb7b14fdf45722d58db32ba20107a8ff4657", + "variant": "debug" + }, + "cpython-3.14.0b3+debug-linux-x86_64_v2-musl": { + "name": "cpython", + "arch": { + "family": "x86_64", + "variant": "v2" + }, + "os": "linux", + "libc": "musl", + "major": 3, + "minor": 14, + "patch": 0, + "prerelease": "b3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-x86_64_v2-unknown-linux-musl-debug-full.tar.zst", + "sha256": "88233f533101941f03aef44bb58e44c6e9a2f7802ae85294ff3804472136d363", + "variant": "debug" + }, + "cpython-3.14.0b3+debug-linux-x86_64_v3-gnu": { + "name": "cpython", + "arch": { + "family": "x86_64", + "variant": "v3" + }, + "os": "linux", + "libc": "gnu", + "major": 3, + "minor": 14, + "patch": 0, + "prerelease": "b3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "edd1800dd1d2abc38db383d7ff61bb21597f608a64ab44cc23f009af0291a96c", + "variant": "debug" + }, + "cpython-3.14.0b3+debug-linux-x86_64_v3-musl": { + "name": "cpython", + "arch": { + "family": "x86_64", + "variant": "v3" + }, + "os": "linux", + "libc": "musl", + "major": 3, + "minor": 14, + "patch": 0, + "prerelease": "b3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-x86_64_v3-unknown-linux-musl-debug-full.tar.zst", + "sha256": "6ceeb66c4876a7484b8ba9847e590d74ca35b67cbe692d06b3d3466a483406f8", + "variant": "debug" + }, + "cpython-3.14.0b3+debug-linux-x86_64_v4-gnu": { + "name": "cpython", + "arch": { + "family": "x86_64", + "variant": "v4" + }, + "os": "linux", + "libc": "gnu", + "major": 3, + "minor": 14, + "patch": 0, + "prerelease": "b3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "2f4d7ced306f9d4c800e9d7a86183c5f92c7858d8f64e38afd73db45414cbb82", + "variant": "debug" + }, + "cpython-3.14.0b3+debug-linux-x86_64_v4-musl": { + "name": "cpython", + "arch": { + "family": "x86_64", + "variant": "v4" + }, + "os": "linux", + "libc": "musl", + "major": 3, + "minor": 14, + "patch": 0, + "prerelease": "b3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-x86_64_v4-unknown-linux-musl-debug-full.tar.zst", + "sha256": "40191a40700aa65c0f92fcea8cd63de1d840ca5913c11b0226e677e1617dbf5d", + "variant": "debug" + }, "cpython-3.14.0b2-darwin-aarch64-none": { "name": "cpython", "arch": { @@ -4731,8 +5531,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.13.5%2B20250612-aarch64-apple-darwin-install_only_stripped.tar.gz", - "sha256": "3e52e6b539dca2729788a06f3f47b2edfc30ba3ef82eb14926f0a23ed0ce4cff", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-aarch64-apple-darwin-install_only_stripped.tar.gz", + "sha256": "2303b54d2780aeac8454b000515ea37c1ce9391dd0719bbf4f9aad453c4460fc", "variant": null }, "cpython-3.13.5-darwin-x86_64-none": { @@ -4747,8 +5547,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.13.5%2B20250612-x86_64-apple-darwin-install_only_stripped.tar.gz", - "sha256": "fce29c000087f0ed966580aff703117d8238e2be043a90a2a0ec8352c0708db8", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-x86_64-apple-darwin-install_only_stripped.tar.gz", + "sha256": "baf14de314f191fd168fae778796c64e40667b28b8c78ae8b2bc340552e88a9a", "variant": null }, "cpython-3.13.5-linux-aarch64-gnu": { @@ -4763,8 +5563,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.13.5%2B20250612-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "6957a6d66c633890fc97f3da818066cd0d10de7cf695a7c46c4c23b107c02fa7", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "bef5e63e7c8c70c2336525ffd6758da801942127ce9f6c7c378fecc4ed09716d", "variant": null }, "cpython-3.13.5-linux-armv7-gnueabi": { @@ -4779,8 +5579,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.13.5%2B20250612-armv7-unknown-linux-gnueabi-install_only_stripped.tar.gz", - "sha256": "1ca9efecff540162b22e5b86152864e621c97463061171f6734cd31d50e39f1d", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-armv7-unknown-linux-gnueabi-install_only_stripped.tar.gz", + "sha256": "ffa3f4b9a45bfac9e4ba045d2419354fccd2e716ffa191eccf0ec0d57af7ad8d", "variant": null }, "cpython-3.13.5-linux-armv7-gnueabihf": { @@ -4795,8 +5595,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.13.5%2B20250612-armv7-unknown-linux-gnueabihf-install_only_stripped.tar.gz", - "sha256": "4efad529678f93119e99bd5494070c76f5c54958bc9686ee12fd9e1950c80b27", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-armv7-unknown-linux-gnueabihf-install_only_stripped.tar.gz", + "sha256": "f466eef0279076db5d929081dd287f98c7904fc713dd833a2ba6df4250a3b23e", "variant": null }, "cpython-3.13.5-linux-powerpc64le-gnu": { @@ -4811,8 +5611,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.13.5%2B20250612-ppc64le-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "abbd685fe948653ad79624e568f9f843233e808c8756d6d4429dbe1d3e7550f9", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-ppc64le-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "3a1a91c3ac60188e27d87fb3487da1dd452bff984c9ed09c419d38c3c373eea7", "variant": null }, "cpython-3.13.5-linux-riscv64-gnu": { @@ -4827,8 +5627,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.13.5%2B20250612-riscv64-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "4a17bcc199782d0bbaaf073b0eedfac0ebfc5eeab2cc23b9b59968869708779c", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-riscv64-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "8b5b76554004afec79469408f3facaa0407fac953c1e39badcd69fb4cbe9e7e3", "variant": null }, "cpython-3.13.5-linux-s390x-gnu": { @@ -4843,8 +5643,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.13.5%2B20250612-s390x-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "6d877e1b2c302d205f0bddbc76b7ca465fa227d9252147df7821d5474d4ea147", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-s390x-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "9da68c953563b4ff3bf21a014350d76a20da5c5778ed433216f8c2ebc8bfd12b", "variant": null }, "cpython-3.13.5-linux-x86_64-gnu": { @@ -4859,8 +5659,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.13.5%2B20250612-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "5c63e7ffe47baff0a96a685c94fb5075612817741feb4e85ec3cc082c742b4f8", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "1894edce20e8df3739d2136368a5c9f38f456f69c9ee4401c01fff4477e2934d", "variant": null }, "cpython-3.13.5-linux-x86_64-musl": { @@ -4875,8 +5675,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.13.5%2B20250612-x86_64-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "641d0cefa3124e42728fc5dac970d5a172a61d80d2a5a24995f2b6e9ddf71e3f", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-x86_64-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "8f50133eda9061154eb82a8807cb41ec64d2e17b429ddfcd1c90062e22d442fa", "variant": null }, "cpython-3.13.5-linux-x86_64_v2-gnu": { @@ -4891,8 +5691,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.13.5%2B20250612-x86_64_v2-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "7f60b2f61fad6c846c7d7c8f523dee285c36cd9d53573314b6ca82eca4e80241", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-x86_64_v2-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "bf77b2a07d8c91b27befc58de9e512340ccf7ee4a365c3cde6a59e28de8b3343", "variant": null }, "cpython-3.13.5-linux-x86_64_v2-musl": { @@ -4907,8 +5707,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.13.5%2B20250612-x86_64_v2-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "21c20b03866e1ec3dcd86224cf82396e58e175768e51030ab83ba21d482cfc26", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-x86_64_v2-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "0e3347d77aa1fc36dd5b7a9dfc29968660c305bd2bb763af4abfab6a7f67a80d", "variant": null }, "cpython-3.13.5-linux-x86_64_v3-gnu": { @@ -4923,8 +5723,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.13.5%2B20250612-x86_64_v3-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "ec8794e649b3e0abac7dccda7de20892ce1ba43f2148e65b2a66edeba42f4c61", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-x86_64_v3-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "75347d44b5f7cf8c0642cb43b77797b15e6346633bc17c0fb47f7968d7221fa9", "variant": null }, "cpython-3.13.5-linux-x86_64_v3-musl": { @@ -4939,8 +5739,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.13.5%2B20250612-x86_64_v3-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "56576436ccc2a9e509becd72ebdbc9abf147a471df6e1022dcd6967975ccee55", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-x86_64_v3-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "9e4191282a518d02db5d7ce5f42d7e96393768243f55e6a6a96e9c50f0cc72fa", "variant": null }, "cpython-3.13.5-linux-x86_64_v4-gnu": { @@ -4955,8 +5755,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.13.5%2B20250612-x86_64_v4-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "b6aa1c65d74b528130244888ee5b47fbf52451aabac2a98e5b87873e73705d87", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-x86_64_v4-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "b841a8f77bedde2882f9bc65393939a49142c599b465c12c1bdd86dde52d6fc8", "variant": null }, "cpython-3.13.5-linux-x86_64_v4-musl": { @@ -4971,8 +5771,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.13.5%2B20250612-x86_64_v4-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "c3322d81ef18e862e0069bfd8824ef1a907135bf2d1c6514312854ea99f0a372", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-x86_64_v4-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "446fb13abdcbf8a329074f9bfb4e9803b292a54f6252948edd08d5cf9c973289", "variant": null }, "cpython-3.13.5-windows-i686-none": { @@ -4987,8 +5787,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.13.5%2B20250612-i686-pc-windows-msvc-install_only_stripped.tar.gz", - "sha256": "254754ec03dd94dc8e67f89b415253a9ee16f0d277478e0e01c25de45b7fc172", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-i686-pc-windows-msvc-install_only_stripped.tar.gz", + "sha256": "c458f8200dd040af36d087791c48efa239160de641b7fda8d9c1fc6536a5e4a4", "variant": null }, "cpython-3.13.5-windows-x86_64-none": { @@ -5003,8 +5803,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.13.5%2B20250612-x86_64-pc-windows-msvc-install_only_stripped.tar.gz", - "sha256": "361fa8ca96403f890d0ef4f202ea410b2e121273830c979d6517f2c7e733b5e2", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-x86_64-pc-windows-msvc-install_only_stripped.tar.gz", + "sha256": "ab9b81bb06a51791040f6a7e9bffa62eec7ae60041d3cf7409ee7431f2237c7b", "variant": null }, "cpython-3.13.5+freethreaded-darwin-aarch64-none": { @@ -5019,8 +5819,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.13.5%2B20250612-aarch64-apple-darwin-freethreaded%2Bpgo%2Blto-full.tar.zst", - "sha256": "a29cb4ef8adcd343e0f5bc5c4371cbc859fc7ce6d8f1a3c8d0cd7e44c4b9b866", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-aarch64-apple-darwin-freethreaded%2Bpgo%2Blto-full.tar.zst", + "sha256": "7223a0e13d5e290fa8441b5439d08fca6fe389bcc186f918f2edd808027dcd08", "variant": "freethreaded" }, "cpython-3.13.5+freethreaded-darwin-x86_64-none": { @@ -5035,8 +5835,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.13.5%2B20250612-x86_64-apple-darwin-freethreaded%2Bpgo%2Blto-full.tar.zst", - "sha256": "52aeb1b4073fa3f180d74a0712ceabc86dd2b40be499599e2e170948fb22acde", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-x86_64-apple-darwin-freethreaded%2Bpgo%2Blto-full.tar.zst", + "sha256": "869ca9d095f9e8f50fc8609d55d6a937c48a7d0b09e7ab5a3679307f9eb90c70", "variant": "freethreaded" }, "cpython-3.13.5+freethreaded-linux-aarch64-gnu": { @@ -5051,8 +5851,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.13.5%2B20250612-aarch64-unknown-linux-gnu-freethreaded%2Blto-full.tar.zst", - "sha256": "0ef13d13e16b4e58f167694940c6db54591db50bbc7ba61be6901ed5a69ad27b", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-aarch64-unknown-linux-gnu-freethreaded%2Blto-full.tar.zst", + "sha256": "8437225a6066e9f57a2ce631a73eceedffeadfe4146b7861e6ace5647a0472da", "variant": "freethreaded" }, "cpython-3.13.5+freethreaded-linux-armv7-gnueabi": { @@ -5067,8 +5867,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.13.5%2B20250612-armv7-unknown-linux-gnueabi-freethreaded%2Blto-full.tar.zst", - "sha256": "4eb024f92a1e832c7533d17d566c47eabcb7b5684112290695ef76a149282ee4", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-armv7-unknown-linux-gnueabi-freethreaded%2Blto-full.tar.zst", + "sha256": "eabb70dff454bf923a728853c840ee318bc5a0061d988900f225de5b1eb4058b", "variant": "freethreaded" }, "cpython-3.13.5+freethreaded-linux-armv7-gnueabihf": { @@ -5083,8 +5883,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.13.5%2B20250612-armv7-unknown-linux-gnueabihf-freethreaded%2Blto-full.tar.zst", - "sha256": "cac9d5fe76dcc4942b538d5f5a9fa6c20f261bc02a8e75821dd2ea4e6c214074", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-armv7-unknown-linux-gnueabihf-freethreaded%2Blto-full.tar.zst", + "sha256": "aa49a863343cbbae5a7a1104adb11e9d1d26843598eb5ba9e3db135d27df721a", "variant": "freethreaded" }, "cpython-3.13.5+freethreaded-linux-powerpc64le-gnu": { @@ -5099,8 +5899,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.13.5%2B20250612-ppc64le-unknown-linux-gnu-freethreaded%2Blto-full.tar.zst", - "sha256": "66545ad4b09385750529ef09a665fc0b0ce698f984df106d7b167e3f7d59eace", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-ppc64le-unknown-linux-gnu-freethreaded%2Blto-full.tar.zst", + "sha256": "09008067d69b833b831cc6090edb221f1cce780c4586db8231dcfb988d1b7571", "variant": "freethreaded" }, "cpython-3.13.5+freethreaded-linux-riscv64-gnu": { @@ -5115,8 +5915,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.13.5%2B20250612-riscv64-unknown-linux-gnu-freethreaded%2Blto-full.tar.zst", - "sha256": "a82a741abefa7db61b2aeef36426bd56da5c69dc9dac105d68fba7fe658943ca", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-riscv64-unknown-linux-gnu-freethreaded%2Blto-full.tar.zst", + "sha256": "3bc92a4057557e9a9f7e8bd8e673dfae54f9abbd14217ae4d986ba29c8c1e761", "variant": "freethreaded" }, "cpython-3.13.5+freethreaded-linux-s390x-gnu": { @@ -5131,8 +5931,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.13.5%2B20250612-s390x-unknown-linux-gnu-freethreaded%2Blto-full.tar.zst", - "sha256": "403c5758428013d5aa472841294c7b6ec91a572bb7123d02b7f1de24af4b0e13", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-s390x-unknown-linux-gnu-freethreaded%2Blto-full.tar.zst", + "sha256": "3206aa76d604d87222ef1cd069b4c7428b3a8f991580504ae21f0926c53a97c5", "variant": "freethreaded" }, "cpython-3.13.5+freethreaded-linux-x86_64-gnu": { @@ -5147,8 +5947,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.13.5%2B20250612-x86_64-unknown-linux-gnu-freethreaded%2Bpgo%2Blto-full.tar.zst", - "sha256": "33fdd6c42258cdf0402297d9e06842b53d9413d70849cee61755b9b5fb619836", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-x86_64-unknown-linux-gnu-freethreaded%2Bpgo%2Blto-full.tar.zst", + "sha256": "a45ffc5a812c3b6db1dce34fc72c35fb3c791075c4602d0fb742c889bc6bf26d", "variant": "freethreaded" }, "cpython-3.13.5+freethreaded-linux-x86_64-musl": { @@ -5163,8 +5963,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.13.5%2B20250612-x86_64-unknown-linux-musl-freethreaded%2Blto-full.tar.zst", - "sha256": "35c387d86e2238d65c16962003475074a771bd96a6e6027606365dd9b23307c2", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-x86_64-unknown-linux-musl-freethreaded%2Blto-full.tar.zst", + "sha256": "c0199c9816dea06a4bc6c5f970d4909660302acb62639a54da7b5d6a4bb45106", "variant": "freethreaded" }, "cpython-3.13.5+freethreaded-linux-x86_64_v2-gnu": { @@ -5179,8 +5979,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.13.5%2B20250612-x86_64_v2-unknown-linux-gnu-freethreaded%2Bpgo%2Blto-full.tar.zst", - "sha256": "5de3a03470af84e56557a5429137f96632536b0dc07ec119988e9936fcd586b5", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-x86_64_v2-unknown-linux-gnu-freethreaded%2Bpgo%2Blto-full.tar.zst", + "sha256": "5ab68785f2b5098e5e984bc49b9df1530bee0097dddcd4fe5f197e48d3e619f2", "variant": "freethreaded" }, "cpython-3.13.5+freethreaded-linux-x86_64_v2-musl": { @@ -5195,8 +5995,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.13.5%2B20250612-x86_64_v2-unknown-linux-musl-freethreaded%2Blto-full.tar.zst", - "sha256": "4e4f198e3cb5248921a7292620156412735b154201571f5da6198167b9888b5c", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-x86_64_v2-unknown-linux-musl-freethreaded%2Blto-full.tar.zst", + "sha256": "b91e0b88eb9295fae197f870f2291a3bd1d47758f2aabc8c2e1996af1b14f180", "variant": "freethreaded" }, "cpython-3.13.5+freethreaded-linux-x86_64_v3-gnu": { @@ -5211,8 +6011,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.13.5%2B20250612-x86_64_v3-unknown-linux-gnu-freethreaded%2Bpgo%2Blto-full.tar.zst", - "sha256": "b826fe552e2fcc12859f963410e2c1a109929fe5b73978a74f64c6c812fef92f", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-x86_64_v3-unknown-linux-gnu-freethreaded%2Bpgo%2Blto-full.tar.zst", + "sha256": "91bdb8c0792ef29cef987b3198e18f9c441794b33f1266c9155530ddfcfa8b3a", "variant": "freethreaded" }, "cpython-3.13.5+freethreaded-linux-x86_64_v3-musl": { @@ -5227,8 +6027,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.13.5%2B20250612-x86_64_v3-unknown-linux-musl-freethreaded%2Blto-full.tar.zst", - "sha256": "20702fe10bef77477eb02c5a1817650560142b477572f391c297e137daf7a057", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-x86_64_v3-unknown-linux-musl-freethreaded%2Blto-full.tar.zst", + "sha256": "01191f5268159e7448a320703c40508802baa1780e855771311f01c550d80b58", "variant": "freethreaded" }, "cpython-3.13.5+freethreaded-linux-x86_64_v4-gnu": { @@ -5243,8 +6043,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.13.5%2B20250612-x86_64_v4-unknown-linux-gnu-freethreaded%2Bpgo%2Blto-full.tar.zst", - "sha256": "d4024ab82373300a8c1262033af61f64b3348379afe9a112004cf6988468b551", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-x86_64_v4-unknown-linux-gnu-freethreaded%2Bpgo%2Blto-full.tar.zst", + "sha256": "ebcd45022331fe1ebdee98a3b16c876d8be147079206fa3ccc4d81b89fd7ac8b", "variant": "freethreaded" }, "cpython-3.13.5+freethreaded-linux-x86_64_v4-musl": { @@ -5259,8 +6059,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.13.5%2B20250612-x86_64_v4-unknown-linux-musl-freethreaded%2Blto-full.tar.zst", - "sha256": "ab04b63ce58c9ff63c5314ad32a28b25b0b1dbd0a521e1ad056550142f55de43", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-x86_64_v4-unknown-linux-musl-freethreaded%2Blto-full.tar.zst", + "sha256": "d30c596a8116a92f860d38b5d5a11e6dd5dea2bbbcdf7439e3223e357298b838", "variant": "freethreaded" }, "cpython-3.13.5+freethreaded-windows-i686-none": { @@ -5275,8 +6075,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.13.5%2B20250612-i686-pc-windows-msvc-freethreaded%2Bpgo-full.tar.zst", - "sha256": "9256369550eeb71729dca0094d098d161035806c24b9b9054cb8038b05bd7e0f", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-i686-pc-windows-msvc-freethreaded%2Bpgo-full.tar.zst", + "sha256": "d1d421904aa75fce2fb5cb59bacb83787fbe7fbc68dc355c7fdbd19094030473", "variant": "freethreaded" }, "cpython-3.13.5+freethreaded-windows-x86_64-none": { @@ -5291,8 +6091,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.13.5%2B20250612-x86_64-pc-windows-msvc-freethreaded%2Bpgo-full.tar.zst", - "sha256": "9da2f02d81597340163174ee91d91a8733dad2af53fc1b7c79ecc45a739a89d5", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-x86_64-pc-windows-msvc-freethreaded%2Bpgo-full.tar.zst", + "sha256": "79c5594d758c7db8323abc23325e17955a6c6e300fec04abdeecf29632de1e34", "variant": "freethreaded" }, "cpython-3.13.5+debug-linux-aarch64-gnu": { @@ -5307,8 +6107,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.13.5%2B20250612-aarch64-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "6280fc111ff607571362c12e8b1b12a3799a3fbec60498bd0ebff77d30efa89f", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-aarch64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "bbc5dab19b3444b8056e717babc81d948b4a45b8f1a6e25d1c193fcaf572bf25", "variant": "debug" }, "cpython-3.13.5+debug-linux-armv7-gnueabi": { @@ -5323,8 +6123,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.13.5%2B20250612-armv7-unknown-linux-gnueabi-debug-full.tar.zst", - "sha256": "48b4fb6454358be8383598876d389fcf5cb5144af07d200e0b0e7c7824084e3e", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-armv7-unknown-linux-gnueabi-debug-full.tar.zst", + "sha256": "2be85ab8daa6c898292789d7fe83d939090a44681b0424789fba278a7dded5fd", "variant": "debug" }, "cpython-3.13.5+debug-linux-armv7-gnueabihf": { @@ -5339,8 +6139,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.13.5%2B20250612-armv7-unknown-linux-gnueabihf-debug-full.tar.zst", - "sha256": "bd5df3367a45decbb740432d8e838975ac58c40fc495ed54dbbe321dccb0cd44", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-armv7-unknown-linux-gnueabihf-debug-full.tar.zst", + "sha256": "372ca41f12f67a44d0aedd28714daade9d35b4c14157ea4cdd5b12eea3f5ddf8", "variant": "debug" }, "cpython-3.13.5+debug-linux-powerpc64le-gnu": { @@ -5355,8 +6155,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.13.5%2B20250612-ppc64le-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "e95e45ad547ab71a919a22d76b065881926a00920c5cc1ee6d97be4d66daa12d", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-ppc64le-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "0cb23ab284eab18f471716f4aa1ba4ee570a75a6d79fa5cd091264c514e54a91", "variant": "debug" }, "cpython-3.13.5+debug-linux-riscv64-gnu": { @@ -5371,8 +6171,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.13.5%2B20250612-riscv64-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "ba433760881a5b0da9b46dcdcf2dd8ca6917901e78dbac74c4ba3ab5e6f3ced3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-riscv64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "315184f457d26e6e20cf69ec169f3fdfdd232e6766a29905cbb6044501fcd4e5", "variant": "debug" }, "cpython-3.13.5+debug-linux-s390x-gnu": { @@ -5387,8 +6187,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.13.5%2B20250612-s390x-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "f6f6f8187ede00fa3938d4c734008eafec2041636a8987e2c85df3273004b822", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-s390x-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "8bc2ca17f35f14b6f07882e75c7034263e23fbe003e275e068f2d84e823ed2bf", "variant": "debug" }, "cpython-3.13.5+debug-linux-x86_64-gnu": { @@ -5403,8 +6203,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.13.5%2B20250612-x86_64-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "137c6262fa2b26f20d7e1bed1c1467a7665086bb88dc1c2cb40cf23e7da6d469", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-x86_64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "994e77601b9c4f9f48000f575e1d1c640b8b3d795fb65b6b4656b7664df2f95c", "variant": "debug" }, "cpython-3.13.5+debug-linux-x86_64-musl": { @@ -5419,8 +6219,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.13.5%2B20250612-x86_64-unknown-linux-musl-debug-full.tar.zst", - "sha256": "f3c2dce0fb4b62106d3957fc23b757a604251ff624a0d66ef126fab4ece9de0c", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-x86_64-unknown-linux-musl-debug-full.tar.zst", + "sha256": "50027e1a8a82d7924993b49941c6c6fdeeee282cb31807021c44459637b1ca1e", "variant": "debug" }, "cpython-3.13.5+debug-linux-x86_64_v2-gnu": { @@ -5435,8 +6235,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.13.5%2B20250612-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "962846d36bbb78d76f6ac1db77fb37bb9fdda4d545d211048cc3212247890845", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "e9c3d4fdaabe465969533d0129966957088a609814f5f909e25b1397d4fbe960", "variant": "debug" }, "cpython-3.13.5+debug-linux-x86_64_v2-musl": { @@ -5451,8 +6251,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.13.5%2B20250612-x86_64_v2-unknown-linux-musl-debug-full.tar.zst", - "sha256": "515b5cdbb612b606a2215aa2ce94114a2442b34e96a1fcc4a45cb3944a0cc159", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-x86_64_v2-unknown-linux-musl-debug-full.tar.zst", + "sha256": "1e16597f1d1a72a9ffde00d9d76eda5fdd269d3c69d40b1ab1ccb0ee2e2aafcd", "variant": "debug" }, "cpython-3.13.5+debug-linux-x86_64_v3-gnu": { @@ -5467,8 +6267,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.13.5%2B20250612-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "7e7ec18893cff4597a6268416baba95ac640644527ae7531e074a787819eff8e", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "ab9f5380fe0f3d34bb6c02d29103adf2d3b21f47a5f08613fe4f1d7b69d708b4", "variant": "debug" }, "cpython-3.13.5+debug-linux-x86_64_v3-musl": { @@ -5483,8 +6283,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.13.5%2B20250612-x86_64_v3-unknown-linux-musl-debug-full.tar.zst", - "sha256": "bed63cf51a8ef135e7a03aa303c7e5ee76934cd845e946a64033be8b4d3ea246", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-x86_64_v3-unknown-linux-musl-debug-full.tar.zst", + "sha256": "11d126eb8cf367b904982d3b7581bd9bf77630774a1c68bb3290c2372f01360c", "variant": "debug" }, "cpython-3.13.5+debug-linux-x86_64_v4-gnu": { @@ -5499,8 +6299,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.13.5%2B20250612-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "98d2a0c121042905aa874d2afd442098a99d3e00e16af16d940e9460339c7f73", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "8f0bf5fdfd697d5fdd85f9beca4937e9cf93e213d27f46d782d35c30ca2876f6", "variant": "debug" }, "cpython-3.13.5+debug-linux-x86_64_v4-musl": { @@ -5515,8 +6315,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.13.5%2B20250612-x86_64_v4-unknown-linux-musl-debug-full.tar.zst", - "sha256": "d0125aa6426294f2b66a5ab39a13e392d93ff2e61d7304814fae937297c0d45f", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-x86_64_v4-unknown-linux-musl-debug-full.tar.zst", + "sha256": "6fd687f89251c13df002d6bff0f57cbe199f724679df3b8de9bbabafb735d47b", "variant": "debug" }, "cpython-3.13.4-darwin-aarch64-none": { @@ -9739,8 +10539,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.12.11%2B20250612-aarch64-apple-darwin-install_only_stripped.tar.gz", - "sha256": "74dd3b2bbbcb5c87a5044e1f3513fe3b07e72fcfdeb039d0ae83b754911ac31e", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-aarch64-apple-darwin-install_only_stripped.tar.gz", + "sha256": "4919401e556a2720e2fd75635485ef5a6bb4caedcaa228b49acdd060c1b89f4e", "variant": null }, "cpython-3.12.11-darwin-x86_64-none": { @@ -9755,8 +10555,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.12.11%2B20250612-x86_64-apple-darwin-install_only_stripped.tar.gz", - "sha256": "16797cdee1b879ce0f32d9162f2a3af8b91d8ccb663c75ed3afc2384845c24d7", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-x86_64-apple-darwin-install_only_stripped.tar.gz", + "sha256": "4d5d672de6a6f2048f875ea0e6b02c9a4a0248d3d57271c740a1b877442552a1", "variant": null }, "cpython-3.12.11-linux-aarch64-gnu": { @@ -9771,8 +10571,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.12.11%2B20250612-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "df383a0992be93314880232c2ecbe9764ee65caee5f72a13ef672684fc7b8063", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "e6797c50c9ce77904e63b8c15cc97a9ff459006bea726bf2ce41ba2ef317d4af", "variant": null }, "cpython-3.12.11-linux-armv7-gnueabi": { @@ -9787,8 +10587,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.12.11%2B20250612-armv7-unknown-linux-gnueabi-install_only_stripped.tar.gz", - "sha256": "92c9c4bd3e4f8bd086ae7ff234273898e83340e4d65fa5b50b0e87db8197fdff", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-armv7-unknown-linux-gnueabi-install_only_stripped.tar.gz", + "sha256": "60c8843e9e70610f3d82f26b0ba395312382d36e3078d141896ab4aabd422be8", "variant": null }, "cpython-3.12.11-linux-armv7-gnueabihf": { @@ -9803,8 +10603,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.12.11%2B20250612-armv7-unknown-linux-gnueabihf-install_only_stripped.tar.gz", - "sha256": "197e34e0a74504f2700d4e4c11cb0d281aa13c628af8b9ad21532250bda45659", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-armv7-unknown-linux-gnueabihf-install_only_stripped.tar.gz", + "sha256": "d97c4bcb8514bf2f7b202e018a2b99e9a4ab797bf1b0c31990641fe9652cae2e", "variant": null }, "cpython-3.12.11-linux-powerpc64le-gnu": { @@ -9819,8 +10619,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.12.11%2B20250612-ppc64le-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "f4bce8f1dcf7bef91d1ea54af48a45333983a41b83c0b8e33e9b07bb4b4499a0", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-ppc64le-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "bebc3aa45ff2d228f7378faedf2556a681e9c626b8e237d39cf4eb6552438242", "variant": null }, "cpython-3.12.11-linux-riscv64-gnu": { @@ -9835,8 +10635,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.12.11%2B20250612-riscv64-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "26afb0f604cd9cac7af5e3078bbdcb7f701cd1f4956fba0620cc184bc9b32927", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-riscv64-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "e82a07c281bef00d14a6bf65b644a4758ee0c8105e0aa549c49471cc4656046f", "variant": null }, "cpython-3.12.11-linux-s390x-gnu": { @@ -9851,8 +10651,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.12.11%2B20250612-s390x-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "1c7327875d7669862fd1627c57a813378d866998c5d5008276c8952af7323d19", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-s390x-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "ccd091fd9a8cc1a57da15d1e8ae523b6363c2ce9a0599ac1b63f5c8b70767268", "variant": null }, "cpython-3.12.11-linux-x86_64-gnu": { @@ -9867,8 +10667,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.12.11%2B20250612-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "15a3c9964e485f04d3c92739aca190616e09b2c4fac29b263432f6f29f00c6cf", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "4ab7c1866d0b072b75d5d7b010f73e6539309c611ecad55852411fc8b0b44541", "variant": null }, "cpython-3.12.11-linux-x86_64-musl": { @@ -9883,8 +10683,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.12.11%2B20250612-x86_64-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "0b68e1de34febc8c57df3d0bf13e3397493bacc432b4cc3d27a338c2d4b8a428", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-x86_64-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "2fe4faa95b605a4616c3c3d986fb8ce9a31c16a68b6e77f56c68468a87dff29e", "variant": null }, "cpython-3.12.11-linux-x86_64_v2-gnu": { @@ -9899,8 +10699,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.12.11%2B20250612-x86_64_v2-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "351b7a97142bc0539ef67e1ad61961a99df419487af422b2242664702f3d3fde", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-x86_64_v2-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "531c60132607239f677d6bb91d33645cd7564f1f563e43fff889c67589d6e092", "variant": null }, "cpython-3.12.11-linux-x86_64_v2-musl": { @@ -9915,8 +10715,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.12.11%2B20250612-x86_64_v2-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "2a4d4edb63585bbfc4afa4bddd5e3efb20202903925ace6f0797df1ad2a6189d", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-x86_64_v2-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "c2a86e3077ccda859a8669896a4c41ea167c03b105c4307523e6e0b0bc820c25", "variant": null }, "cpython-3.12.11-linux-x86_64_v3-gnu": { @@ -9931,8 +10731,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.12.11%2B20250612-x86_64_v3-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "bb742a2e0bc09b0afd4c37056ea0bda16095d8468442eadb6285716d0fcb8ab0", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-x86_64_v3-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "07015ad91ec3ee89c410b09bae84a5910037766783ae47bfb09469f80e1f2337", "variant": null }, "cpython-3.12.11-linux-x86_64_v3-musl": { @@ -9947,8 +10747,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.12.11%2B20250612-x86_64_v3-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "5bf4aab9fea91a6daae95f40cd12c7d4127bed98bc5ea4fcbee9f03afc1530ef", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-x86_64_v3-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "891ba6c18dd688cf7b633ff2bb40aecf007d439c6732b2a9b8de78ffbb0b9421", "variant": null }, "cpython-3.12.11-linux-x86_64_v4-gnu": { @@ -9963,8 +10763,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.12.11%2B20250612-x86_64_v4-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "05f1a3f7711aae5c65e4557ea102315a779cbe03e39f16dc405f8fc8ede25e83", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-x86_64_v4-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "b7f5456cb566bf05bf1f1818eb2dda4f76a3c6257697f31d01ada063ec324f96", "variant": null }, "cpython-3.12.11-linux-x86_64_v4-musl": { @@ -9979,8 +10779,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.12.11%2B20250612-x86_64_v4-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "d277d0d6d58436ca6e46b7d5d9e1758a89e6b90a0524d789646a4a589c0be998", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-x86_64_v4-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "55f5121910e7c9229890e59407dc447807bee7173dc979af7cab8ea6ddd36881", "variant": null }, "cpython-3.12.11-windows-i686-none": { @@ -9995,8 +10795,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.12.11%2B20250612-i686-pc-windows-msvc-install_only_stripped.tar.gz", - "sha256": "41d5dd36a6964f37b709061c5c01429579ef3c3e117ed7043d6a3a1f336671d6", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-i686-pc-windows-msvc-install_only_stripped.tar.gz", + "sha256": "9ddbc2d11445007c8c44ecc8fb23b60c19143e7ff2bc99f9a6d7ab19cf18825e", "variant": null }, "cpython-3.12.11-windows-x86_64-none": { @@ -10011,8 +10811,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.12.11%2B20250612-x86_64-pc-windows-msvc-install_only_stripped.tar.gz", - "sha256": "51bc462f3d6caf4aef3d77209d01cd5f6c8fe8213c1ae739e573e1c2c473cb2b", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-x86_64-pc-windows-msvc-install_only_stripped.tar.gz", + "sha256": "429e1cc78f4b7591b4ec2063d334dd0bb85afb41e246af1e2b919acdf99fc1b0", "variant": null }, "cpython-3.12.11+debug-linux-aarch64-gnu": { @@ -10027,8 +10827,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.12.11%2B20250612-aarch64-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "208f407d3880dc84772d8a322776011abf362ac004540debbd2ea5e5f884f398", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-aarch64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "bc8c87d783ae3685df5a0954da4de6513cd0e82135c115c7d472adbabb9c992d", "variant": "debug" }, "cpython-3.12.11+debug-linux-armv7-gnueabi": { @@ -10043,8 +10843,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.12.11%2B20250612-armv7-unknown-linux-gnueabi-debug-full.tar.zst", - "sha256": "880268f3e83952d5bdb32422da2ce7f24ee24c6251b946514ffcdbaedc4ced37", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-armv7-unknown-linux-gnueabi-debug-full.tar.zst", + "sha256": "40f70903123cf16fb677190388f1a63e45458c2b2ae106c8ddb3ef32ace4c8d1", "variant": "debug" }, "cpython-3.12.11+debug-linux-armv7-gnueabihf": { @@ -10059,8 +10859,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.12.11%2B20250612-armv7-unknown-linux-gnueabihf-debug-full.tar.zst", - "sha256": "d3128aba3c94c46d0f5d15474af6a8b340b08ada33a31ad20387cfa46891752c", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-armv7-unknown-linux-gnueabihf-debug-full.tar.zst", + "sha256": "fec4a3348980ecbae93e241946bd825f57f26b5a1b99cc56ee4e6d4a3dd53f3c", "variant": "debug" }, "cpython-3.12.11+debug-linux-powerpc64le-gnu": { @@ -10075,8 +10875,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.12.11%2B20250612-ppc64le-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "3c43d94ef5fc2db0fd937f16616c9aceaf0ebc4846ae9454190ed30b0ad4830c", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-ppc64le-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "dea804e42c48de02e12fda2726dea59aa05091c792a2defe0c42637658711c46", "variant": "debug" }, "cpython-3.12.11+debug-linux-riscv64-gnu": { @@ -10091,8 +10891,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.12.11%2B20250612-riscv64-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "be24538f1d59620a3229582f7adf9ca0df3129bd6591ff45b6ce42d1bb57602f", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-riscv64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "128949afd8c2ead23abf0210faa20cbd46408e66bba1cc8950b4fcdb54cea8fa", "variant": "debug" }, "cpython-3.12.11+debug-linux-s390x-gnu": { @@ -10107,8 +10907,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.12.11%2B20250612-s390x-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "fa5095dd99ffa5e15dbe151c3d98dbe534c230ce6b9669eef34e037fc885ed91", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-s390x-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "b025e7f6acad441a17394d0fc24f696717dd9ec93718000379ca376a02d7c4a6", "variant": "debug" }, "cpython-3.12.11+debug-linux-x86_64-gnu": { @@ -10123,8 +10923,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.12.11%2B20250612-x86_64-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "b1b114b14624ec9955ea1404908636580280a8537ce85ace2fd48197edf82ee0", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-x86_64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "99ab13c789dffdf0b43e0c206fd69c73a6713527bf74c984c7e99bab8362ab45", "variant": "debug" }, "cpython-3.12.11+debug-linux-x86_64-musl": { @@ -10139,8 +10939,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.12.11%2B20250612-x86_64-unknown-linux-musl-debug-full.tar.zst", - "sha256": "8535b0850eab8f8cf6e29a5642f7501f5de0318d7805de6aa2cc69c0b7f4895d", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-x86_64-unknown-linux-musl-debug-full.tar.zst", + "sha256": "53e842d33cc5a61ee2e954a431ea100a59fa5ae53e355d5f34c00c1555a810ce", "variant": "debug" }, "cpython-3.12.11+debug-linux-x86_64_v2-gnu": { @@ -10155,8 +10955,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.12.11%2B20250612-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "d701b1a19ce8fd94c01a72cbe70dd0b79cb59686a7f2fd28c8d68c38d7603f44", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "790b88b4acfc9f58ce1355aba4c73999adf733ac7f8ef87b92b4b092003a0f05", "variant": "debug" }, "cpython-3.12.11+debug-linux-x86_64_v2-musl": { @@ -10171,8 +10971,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.12.11%2B20250612-x86_64_v2-unknown-linux-musl-debug-full.tar.zst", - "sha256": "584bfb7e4d2bd9232072ab51fef342ba250d288cb97066a88713de33280332ad", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-x86_64_v2-unknown-linux-musl-debug-full.tar.zst", + "sha256": "88e92e303d1f61f232f15fe3b266abf49981430d4082451dfda3b0c16900f394", "variant": "debug" }, "cpython-3.12.11+debug-linux-x86_64_v3-gnu": { @@ -10187,8 +10987,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.12.11%2B20250612-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "b7bdaf17df1f0bb374f2553d80e69bd44e6bbc1776a00253661eddbccffc7194", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "701553f500dc2f525ce6a4c78891db14942b1a58fb1b5fa4c4c63120208e9edb", "variant": "debug" }, "cpython-3.12.11+debug-linux-x86_64_v3-musl": { @@ -10203,8 +11003,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.12.11%2B20250612-x86_64_v3-unknown-linux-musl-debug-full.tar.zst", - "sha256": "ee50314ad02b40bf613e9247718a77ac6b5c61e09254c772a5ccea4a3b446262", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-x86_64_v3-unknown-linux-musl-debug-full.tar.zst", + "sha256": "8e3c803ed8d3a32800b1d1b5c3279e11aac1ee07bf977288f77d827ab210794f", "variant": "debug" }, "cpython-3.12.11+debug-linux-x86_64_v4-gnu": { @@ -10219,8 +11019,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.12.11%2B20250612-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "7e16a050d0d0f91312048cb98a1f06d7e300c4723076fdf28d28fa282c45f8a2", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "c66f966c7d32e36851126756ba6ce7cfc6ec0fd2f257deb22420b98fb1790364", "variant": "debug" }, "cpython-3.12.11+debug-linux-x86_64_v4-musl": { @@ -10235,8 +11035,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.12.11%2B20250612-x86_64_v4-unknown-linux-musl-debug-full.tar.zst", - "sha256": "44458a2a2ef2e1bbc4fb226b479d5d00a2fc15788c8b970d8df843e3535b0595", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-x86_64_v4-unknown-linux-musl-debug-full.tar.zst", + "sha256": "438d8cfb97c881636811adf2bceaac28c756cccf852c460716d5f616efae3698", "variant": "debug" }, "cpython-3.12.10-darwin-aarch64-none": { @@ -14283,8 +15083,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.11.13%2B20250612-aarch64-apple-darwin-install_only_stripped.tar.gz", - "sha256": "3cc448659ee0d6aff8a90ca0dcaf00c29974f5d48ccc2c37e7a6e3baa6806005", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-aarch64-apple-darwin-install_only_stripped.tar.gz", + "sha256": "e484c41e7a5c35b2956ac547c4f16fc2f3b4279b480ba3b89c8aef091aa4b51d", "variant": null }, "cpython-3.11.13-darwin-x86_64-none": { @@ -14299,8 +15099,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.11.13%2B20250612-x86_64-apple-darwin-install_only_stripped.tar.gz", - "sha256": "6617e8e95ccbbd27fc82f29b0e56e9d9b8a346435c3510374e4410bfd1150421", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-x86_64-apple-darwin-install_only_stripped.tar.gz", + "sha256": "1d18084cfa3347dc5e1a343cfd42d03de7ef69c93bf5ad31b105cfe12d7e502d", "variant": null }, "cpython-3.11.13-linux-aarch64-gnu": { @@ -14315,8 +15115,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.11.13%2B20250612-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "ac7257a5c1c9757ce4aa61d6c9bc443cd8ab052105b0e1c6714040c6e9e50eff", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "8c26b055937a25ccf17b7578bad42ce6c9fb452a6d4b1d72816755e234922668", "variant": null }, "cpython-3.11.13-linux-armv7-gnueabi": { @@ -14331,8 +15131,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.11.13%2B20250612-armv7-unknown-linux-gnueabi-install_only_stripped.tar.gz", - "sha256": "0b01b96e0e4190f64fef6e2c76d0746321fc8cc91c7f3319452a90eaaa376c00", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-armv7-unknown-linux-gnueabi-install_only_stripped.tar.gz", + "sha256": "17e6023581689c1bd51f37a381e0700770e387d7696bf8d6c7dae3bcea7fdd61", "variant": null }, "cpython-3.11.13-linux-armv7-gnueabihf": { @@ -14347,8 +15147,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.11.13%2B20250612-armv7-unknown-linux-gnueabihf-install_only_stripped.tar.gz", - "sha256": "f30cc16c9ea9a2795b5cafef91f3637165781a6a7a54fdc4baf6438a0800e7ce", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-armv7-unknown-linux-gnueabihf-install_only_stripped.tar.gz", + "sha256": "5a26f2dac227f6667724567027a4b502dea2e27b1105110a71d5a5df0b144a88", "variant": null }, "cpython-3.11.13-linux-powerpc64le-gnu": { @@ -14363,8 +15163,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.11.13%2B20250612-ppc64le-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "472ec854f7944528f366b08e8f6efbb4c02ed265eecc259c0e1f7cf12400ea14", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-ppc64le-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "8273efc027cce7fb3e5ec2a283be0266db76a75dea79bccf24470ff412835305", "variant": null }, "cpython-3.11.13-linux-riscv64-gnu": { @@ -14379,8 +15179,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.11.13%2B20250612-riscv64-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "024ba68fc755b595f4a21dbc1d8744231e51b76111d8d83e96691fb7acbf37a5", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-riscv64-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "cf4719c427131c8d716b0642ddfc84d08d0e870f29cc54c230f3db188da44c72", "variant": null }, "cpython-3.11.13-linux-s390x-gnu": { @@ -14395,8 +15195,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.11.13%2B20250612-s390x-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "6f404269ce33fe0dd8cc918da001662b6ffdfbe7eb13f906cbc92e238f75f6be", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-s390x-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "2ec2fd56109668b455effb6201c9ab716d66bd59e1d144fa2ab4f727afa61c19", "variant": null }, "cpython-3.11.13-linux-x86_64-gnu": { @@ -14411,8 +15211,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.11.13%2B20250612-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "059cbbfd84bfc9ef8a92605fa8aef645bbb45b792cac8adf865050a5e7d68909", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "c6dd8a6fef05c28710ec62fab10d9343b61bbb9f40d402dad923497d7429fd17", "variant": null }, "cpython-3.11.13-linux-x86_64-musl": { @@ -14427,8 +15227,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.11.13%2B20250612-x86_64-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "0237b76f625f08683a2e615ae907428240d90898b17a60bdec88a85bf9095799", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-x86_64-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "3d4fcdcd36591d80ca3b0affb6f96d5c5f56c5a344efbd013e406c9346053005", "variant": null }, "cpython-3.11.13-linux-x86_64_v2-gnu": { @@ -14443,8 +15243,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.11.13%2B20250612-x86_64_v2-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "9ae198daf242ffd6ad5b395594aa157aba62a044d007202cb03659fbb94d3132", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-x86_64_v2-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "9cc3c80f518031a0f2af702a38e37d706015bf411144ca29e05756eeee0f32b2", "variant": null }, "cpython-3.11.13-linux-x86_64_v2-musl": { @@ -14459,8 +15259,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.11.13%2B20250612-x86_64_v2-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "a74c87f366001349fcf3297b87698ac879256ed4b73776ff8fa145c457c0cd13", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-x86_64_v2-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "1f1453776244baff8ff7a17d88fd68fdd10c08a950c911dd25cc28845c949421", "variant": null }, "cpython-3.11.13-linux-x86_64_v3-gnu": { @@ -14475,8 +15275,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.11.13%2B20250612-x86_64_v3-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "b24df39e08456e50fc99c43e695a46240b11251e8b43666f978ee98ec1197e05", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-x86_64_v3-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "094fef0b6e8d778a61d430a44a784caab0c1be5a2b94d7169b17791c6fdfa2e5", "variant": null }, "cpython-3.11.13-linux-x86_64_v3-musl": { @@ -14491,8 +15291,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.11.13%2B20250612-x86_64_v3-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "2767b36bb48da971dc5af762b42df2c9d1f4839326d26c5a710b543372dcc640", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-x86_64_v3-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "526c2c8088c1a41c4bd362c1d463fccaa37129dfe4f28166eabb726664a7550e", "variant": null }, "cpython-3.11.13-linux-x86_64_v4-gnu": { @@ -14507,8 +15307,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.11.13%2B20250612-x86_64_v4-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "a1478d014d07e4a56f0c136534931989a93110ab0b15a051a33fbf0c22bec0d2", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-x86_64_v4-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "27b7bd55029877537242b702badd96846ba1b41346998dfd8c7be191b6b393fa", "variant": null }, "cpython-3.11.13-linux-x86_64_v4-musl": { @@ -14523,8 +15323,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.11.13%2B20250612-x86_64_v4-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "adb7b67a8522c1ef1b941da9fd47dd7c263c489668a40bc9d0b0e955b0e25b18", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-x86_64_v4-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "f8dc02227a0156fd28652148486878ef7a50de09a5b8373555a0573cc2347f18", "variant": null }, "cpython-3.11.13-windows-i686-none": { @@ -14539,8 +15339,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.11.13%2B20250612-i686-pc-windows-msvc-install_only_stripped.tar.gz", - "sha256": "bdb7c0727af0b0d0d5f0d6b37a3139185a0f9259e4ce70f506c23e29e64fcb0f", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-i686-pc-windows-msvc-install_only_stripped.tar.gz", + "sha256": "0d1eb6a83db554379555279da7a00ef421306b35f5fd2603087a960c77aef5dc", "variant": null }, "cpython-3.11.13-windows-x86_64-none": { @@ -14555,8 +15355,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.11.13%2B20250612-x86_64-pc-windows-msvc-install_only_stripped.tar.gz", - "sha256": "22c2ab8be0d4028ddc115583f2c41c57ee269c115ef48a41ddde9adba5bac15b", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-x86_64-pc-windows-msvc-install_only_stripped.tar.gz", + "sha256": "709f7f3a0913226230f50c132ab6a1a517206c9d0b2e30c1779574913f0ac74b", "variant": null }, "cpython-3.11.13+debug-linux-aarch64-gnu": { @@ -14571,8 +15371,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.11.13%2B20250612-aarch64-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "fbd958ddbe10dd9d9304eb3d3dc5ed233524e1e96746e7703ceafedf2af3b82e", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-aarch64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "09a257ac990d66b64f383767154ab8dde452457fd973e403c3ffe73ea2ef6041", "variant": "debug" }, "cpython-3.11.13+debug-linux-armv7-gnueabi": { @@ -14587,8 +15387,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.11.13%2B20250612-armv7-unknown-linux-gnueabi-debug-full.tar.zst", - "sha256": "b03bcfe961888241beefc5648802041764f3914bcb7aadce8d2cbfffd23694d4", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-armv7-unknown-linux-gnueabi-debug-full.tar.zst", + "sha256": "d8dc85bbec1c157e078c66def67ad65157a96ba19fcde8962131a41f4f600e98", "variant": "debug" }, "cpython-3.11.13+debug-linux-armv7-gnueabihf": { @@ -14603,8 +15403,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.11.13%2B20250612-armv7-unknown-linux-gnueabihf-debug-full.tar.zst", - "sha256": "3b7d013b8c200194962ce7dd699d628282ae4343ecdfe33ab1e4ac3cb513e5a5", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-armv7-unknown-linux-gnueabihf-debug-full.tar.zst", + "sha256": "b27b622310194321853d106f4b344b73592b5ca85b1cc9ed3bdb19bdb3d6f0d0", "variant": "debug" }, "cpython-3.11.13+debug-linux-powerpc64le-gnu": { @@ -14619,8 +15419,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.11.13%2B20250612-ppc64le-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "fc84e9f6c98b76525ae2267d24f27c69da6e1ddd77f1cac2613d8b152fa436f1", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-ppc64le-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "8ba75ecfead212f9f5e9b8e2cbc2ad608f5b6812619da4414fd6b283f2acbf78", "variant": "debug" }, "cpython-3.11.13+debug-linux-riscv64-gnu": { @@ -14635,8 +15435,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.11.13%2B20250612-riscv64-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "fc963382c154fbb924b05895cc9849e83998a32060083f7167ae53f5792ac145", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-riscv64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "c199f80c7d4502ba3d710c4c7450f81642bdac5524079829b7c7b8002b9747a8", "variant": "debug" }, "cpython-3.11.13+debug-linux-s390x-gnu": { @@ -14651,8 +15451,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.11.13%2B20250612-s390x-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "01679e250e8693b9d330967741569822db15693926bcdf8e4875b51a8767e9c1", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-s390x-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "23934c0dc86aeb1c5d0f0877ac8d9d6632627a0b60c9f4f9ad9db41278b6745f", "variant": "debug" }, "cpython-3.11.13+debug-linux-x86_64-gnu": { @@ -14667,8 +15467,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.11.13%2B20250612-x86_64-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "206917fe06cdeeb76abd30e3dd01a71355fd41b685c1dbbddbfd0ad47371d5b6", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-x86_64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "a405a9b2bc94b564a03f351b549c708f686aeade9aec480108b613332cf9cc48", "variant": "debug" }, "cpython-3.11.13+debug-linux-x86_64-musl": { @@ -14683,8 +15483,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.11.13%2B20250612-x86_64-unknown-linux-musl-debug-full.tar.zst", - "sha256": "5b7a576ba0e99e7cf734b5a0f1f0fb48564c42a04d373e1370b22f2e70995d79", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-x86_64-unknown-linux-musl-debug-full.tar.zst", + "sha256": "2b3fecb1717cf632e789c4b8c03eda825f9c3e112cac922d58b42e7eecb8731f", "variant": "debug" }, "cpython-3.11.13+debug-linux-x86_64_v2-gnu": { @@ -14699,8 +15499,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.11.13%2B20250612-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "1b80e41e4620e71adb0418a8bb06ec8999aa0dc69efdec0e44ca28c94543e304", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "ab59a4df1e7d269400116e87daaa25b800ffbb24512b57f8d2aa4adbe3d43577", "variant": "debug" }, "cpython-3.11.13+debug-linux-x86_64_v2-musl": { @@ -14715,8 +15515,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.11.13%2B20250612-x86_64_v2-unknown-linux-musl-debug-full.tar.zst", - "sha256": "113f457a87a866f42662cf5789eace03b7548382e2dd0a6b539338defe621697", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-x86_64_v2-unknown-linux-musl-debug-full.tar.zst", + "sha256": "1fc167ea607ef98b6d0dbac121a9ae8739fdf958f21cbbd60cac47b7ce52b003", "variant": "debug" }, "cpython-3.11.13+debug-linux-x86_64_v3-gnu": { @@ -14731,8 +15531,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.11.13%2B20250612-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "f00ae85aa8d900bf2d4e5711395c13458ff298770281dac908117738491cbe51", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "6140e14e713077169826458a47a7439938b98a73810c5e7a709c9c20161ae186", "variant": "debug" }, "cpython-3.11.13+debug-linux-x86_64_v3-musl": { @@ -14747,8 +15547,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.11.13%2B20250612-x86_64_v3-unknown-linux-musl-debug-full.tar.zst", - "sha256": "9f10f5b6622b17237163ae8bff9ec926ce9c44229055c9233e59f16aa7d22842", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-x86_64_v3-unknown-linux-musl-debug-full.tar.zst", + "sha256": "77cf2b966c521c1036eb599ab19e3f6e0d987dbb9ba5daa86d0b59d7d1a609a1", "variant": "debug" }, "cpython-3.11.13+debug-linux-x86_64_v4-gnu": { @@ -14763,8 +15563,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.11.13%2B20250612-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "6e4de8ff2660b542278c84bf63b0747240da339865123e3a5863de2d60601ba6", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "92583c28fdcbbf716329a6711783e2fb031bb8777e8a99bd0b4d3eed03ec0551", "variant": "debug" }, "cpython-3.11.13+debug-linux-x86_64_v4-musl": { @@ -14779,8 +15579,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.11.13%2B20250612-x86_64_v4-unknown-linux-musl-debug-full.tar.zst", - "sha256": "f973507e2c54a75af1583ae7e5a6bf6ba909c5d0e372f306aa6a4d6be8eb92f9", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-x86_64_v4-unknown-linux-musl-debug-full.tar.zst", + "sha256": "dda82882159f45a926486bc221739c2ff5c0b1d9fa705a51d0f3f729f736162c", "variant": "debug" }, "cpython-3.11.12-darwin-aarch64-none": { @@ -18571,8 +19371,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.10.18%2B20250612-aarch64-apple-darwin-install_only_stripped.tar.gz", - "sha256": "d588e367ad0ccc96080f02a6e534b272e1769aeddc0a2ce46da818c42893ebfd", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-aarch64-apple-darwin-install_only_stripped.tar.gz", + "sha256": "e610b8bb397513908b814cb2a1c9f95e97aac33855840bf69f1442ff040ddd33", "variant": null }, "cpython-3.10.18-darwin-x86_64-none": { @@ -18587,8 +19387,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.10.18%2B20250612-x86_64-apple-darwin-install_only_stripped.tar.gz", - "sha256": "2758bbee1709eb355cf0c84a098cf51807a94e2f818eb15a735b71c366b80f9b", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-x86_64-apple-darwin-install_only_stripped.tar.gz", + "sha256": "5c4d02b1626fd677ee3bc432d77e6dca5bbb9b1f03b258edd4c8cf6902eba902", "variant": null }, "cpython-3.10.18-linux-aarch64-gnu": { @@ -18603,8 +19403,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.10.18%2B20250612-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "89a01966d48e36f5ba206f3861ad41b6246160c3feae98a2ffe0c4ce611acfeb", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "12519960127a5082410496f59928c3ed1c2d37076d6400b22e52ee962e315522", "variant": null }, "cpython-3.10.18-linux-armv7-gnueabi": { @@ -18619,8 +19419,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.10.18%2B20250612-armv7-unknown-linux-gnueabi-install_only_stripped.tar.gz", - "sha256": "a095eaeac162f0a57d85b7f7502621d9b9572a272563a59b89b69ae4217e031e", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-armv7-unknown-linux-gnueabi-install_only_stripped.tar.gz", + "sha256": "6dd0d28f7229d044ec1560e11dcbb5452166921c4a931aeae9b9f08f61451eb9", "variant": null }, "cpython-3.10.18-linux-armv7-gnueabihf": { @@ -18635,8 +19435,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.10.18%2B20250612-armv7-unknown-linux-gnueabihf-install_only_stripped.tar.gz", - "sha256": "559799c5b87d742b3b71dc1e7b015a9cd6d130f7b6afcf6ad8716c75c204d47e", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-armv7-unknown-linux-gnueabihf-install_only_stripped.tar.gz", + "sha256": "810089263699a427f8610070ba7ebad2a78dac894e93a6c055ec28f3303c704e", "variant": null }, "cpython-3.10.18-linux-powerpc64le-gnu": { @@ -18651,8 +19451,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.10.18%2B20250612-ppc64le-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "740a2e5f935c6d7799f959731b7cd8f855c1e572ad43f0ec16417c3390f4551d", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-ppc64le-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "263befb4049ed1a272b9299ce41f5d5ef855998462a620dd6b62ecfde47d5154", "variant": null }, "cpython-3.10.18-linux-riscv64-gnu": { @@ -18667,8 +19467,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.10.18%2B20250612-riscv64-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "00a436a8f8ad236d084a6d6a1308543755d9175e042b89ea3c06cc1b1651e6aa", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-riscv64-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "0999d7be0eb75b096791f2f57369dd1a6f4cd9dc44eb9720886268e3a3adddd7", "variant": null }, "cpython-3.10.18-linux-s390x-gnu": { @@ -18683,8 +19483,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.10.18%2B20250612-s390x-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "d23284bc718fb049dd26056efc632042824409599cae9a4c2362277761b50e94", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-s390x-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "2ac9a0791f893b96c4a48942aa2f19b16ddbf60977db63de513beef279599760", "variant": null }, "cpython-3.10.18-linux-x86_64-gnu": { @@ -18699,8 +19499,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.10.18%2B20250612-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "e884283a9a3bb97e9432bbda0bf274608d8fce2f27795485e4a93bbaef66e5a1", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "9d1a87e52e2be1590a6a5148f1b874ba4155095c11e5afad7cb9618e7a4a1912", "variant": null }, "cpython-3.10.18-linux-x86_64-musl": { @@ -18715,8 +19515,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.10.18%2B20250612-x86_64-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "cea06c4409e889945c448ec223f906e9e996994d6b64f05e9d92dc1b6b46a5f8", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-x86_64-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "c85cac7c12bb3c6bf09801f9b3f95d77acb5aa5de10a85aeceafb026d641c62c", "variant": null }, "cpython-3.10.18-linux-x86_64_v2-gnu": { @@ -18731,8 +19531,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.10.18%2B20250612-x86_64_v2-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "4636b6756eb1a9f6db254aac8ae0007c39614fcbf065daf8dc547530ac77c685", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-x86_64_v2-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "d5f70ab75fc24ab3efa4b2edb14163bb145c1f9d297d03dde6c2a40ccb0eb9ac", "variant": null }, "cpython-3.10.18-linux-x86_64_v2-musl": { @@ -18747,8 +19547,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.10.18%2B20250612-x86_64_v2-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "6e0a68e53059d1ccf5a0efc17d60927e53c13e40b670425c121f35cd3fd10981", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-x86_64_v2-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "7800a067ffb6ebd9c226c075da8c57ff843f9b9e7b89b9c14e013bc33f042e4e", "variant": null }, "cpython-3.10.18-linux-x86_64_v3-gnu": { @@ -18763,8 +19563,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.10.18%2B20250612-x86_64_v3-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "1c164e2eeb586d930a427e58db57b161b8ec4b9adf4d31797fdccf78d373a290", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-x86_64_v3-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "ab36f3e99a1fb89cb225fe20a585068a5b4323d084b4441ce0034177b8d8c3bf", "variant": null }, "cpython-3.10.18-linux-x86_64_v3-musl": { @@ -18779,8 +19579,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.10.18%2B20250612-x86_64_v3-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "4ae64d7a6ba3c3cb056c2e17c18087b1052e13045e4fbb2e62e20947de78c916", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-x86_64_v3-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "7e91185f8d1e614c85447561752476b7c079e63df9a707bb9b4c0f1649446e00", "variant": null }, "cpython-3.10.18-linux-x86_64_v4-gnu": { @@ -18795,8 +19595,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.10.18%2B20250612-x86_64_v4-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "cd64acdb527382c7791188f8b5947d1a9e80375ad933e325fb89a128af60234d", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-x86_64_v4-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "65cfa877bc7899f0a458ff5327311e77e0b70fa1c871aeb6dfa582d23422337e", "variant": null }, "cpython-3.10.18-linux-x86_64_v4-musl": { @@ -18811,8 +19611,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.10.18%2B20250612-x86_64_v4-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "8d300050b13d3674f5d19bf402780ec2fb19bf594dd75fd488b83856ed104def", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-x86_64_v4-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "869b5a0919558fe8bbdac4d75a9e9a114a4aa7ca0905da4243ec1b7e4ff99006", "variant": null }, "cpython-3.10.18-windows-i686-none": { @@ -18827,8 +19627,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.10.18%2B20250612-i686-pc-windows-msvc-install_only_stripped.tar.gz", - "sha256": "3df494fd91ccc55ea0b512f70d4b49b4bee781b6e31bfa65c8d859150b6d3715", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-i686-pc-windows-msvc-install_only_stripped.tar.gz", + "sha256": "a0a5edc7cff851a59dca8471858262a2bb3708b00ad443dd908c3b67384b8ee4", "variant": null }, "cpython-3.10.18-windows-x86_64-none": { @@ -18843,8 +19643,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.10.18%2B20250612-x86_64-pc-windows-msvc-install_only_stripped.tar.gz", - "sha256": "4d6337bfafdb6af5c4c6fdb54fd983ead0c4d23cf40fb6b70fce0bd8b3b46b59", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-x86_64-pc-windows-msvc-install_only_stripped.tar.gz", + "sha256": "d6e97e2984ff157d3b5caf9078eb45613684a39368da2fbac8dd364080322e82", "variant": null }, "cpython-3.10.18+debug-linux-aarch64-gnu": { @@ -18859,8 +19659,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.10.18%2B20250612-aarch64-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "2808899e02b96a4ed8cfe7a4e9e42372c0d8746f7cdbf52d21645bd4da1f9f9f", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-aarch64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "e42a95657c84804f0edf02db89e12e907e1fb9d8c92b707214be7c1b3dc0f4d5", "variant": "debug" }, "cpython-3.10.18+debug-linux-armv7-gnueabi": { @@ -18875,8 +19675,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.10.18%2B20250612-armv7-unknown-linux-gnueabi-debug-full.tar.zst", - "sha256": "e327ab463c630396b9a571393adfce4d63b546a2af280c894fef1c74dbb21223", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-armv7-unknown-linux-gnueabi-debug-full.tar.zst", + "sha256": "e733a7aa76137d5a795ec5db08d5c37d06e111e2343d70308f09789ced7b4d32", "variant": "debug" }, "cpython-3.10.18+debug-linux-armv7-gnueabihf": { @@ -18891,8 +19691,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.10.18%2B20250612-armv7-unknown-linux-gnueabihf-debug-full.tar.zst", - "sha256": "031a467de049990d3133e6ff26612e5d227abda4decfa12ea8340ca8ec7e55d4", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-armv7-unknown-linux-gnueabihf-debug-full.tar.zst", + "sha256": "06ec2977ec2d01f3225272926ab43d66f976f1481d069d9a618a0541b2644451", "variant": "debug" }, "cpython-3.10.18+debug-linux-powerpc64le-gnu": { @@ -18907,8 +19707,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.10.18%2B20250612-ppc64le-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "9b48f5108b419e4371142ec5a65a388473e4990181c82343c3dfaf3d87f02a5a", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-ppc64le-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "2ae4ed1805b647f105db8c41d9ac55fcda2672526b63c2e1aa9d0eb16a537594", "variant": "debug" }, "cpython-3.10.18+debug-linux-riscv64-gnu": { @@ -18923,8 +19723,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.10.18%2B20250612-riscv64-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "8215a063192a64fad2b5838b34d20adcd30da5cc2e9598f469eea8d3f0de09f5", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-riscv64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "c9fdef27a8295c29c4ba6fd2daff344c12471f97ca7a19280c3e0f7955438668", "variant": "debug" }, "cpython-3.10.18+debug-linux-s390x-gnu": { @@ -18939,8 +19739,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.10.18%2B20250612-s390x-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "77b9f02331df8acf41784648f75cc77c5ab854546a405b389067f61ded68a5c6", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-s390x-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "eac2aab5eaea5e8df9c24918c0ade7a003abe6d25390d4a85e7dd8eea6eee1e3", "variant": "debug" }, "cpython-3.10.18+debug-linux-x86_64-gnu": { @@ -18955,8 +19755,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.10.18%2B20250612-x86_64-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "bba5598c6fc8f85e68b590950b5e871143647921197be208a94349d7656eafdf", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-x86_64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "1d651aebc511e100a80142b78d2ea4655a6309f5a87a6cbd557fed5afda28f33", "variant": "debug" }, "cpython-3.10.18+debug-linux-x86_64-musl": { @@ -18971,8 +19771,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.10.18%2B20250612-x86_64-unknown-linux-musl-debug-full.tar.zst", - "sha256": "6c006d1daae48ba176bb85fd0c444914d9e2ee20b58e8131c158bb30fe9097c9", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-x86_64-unknown-linux-musl-debug-full.tar.zst", + "sha256": "c84b94373b1704d5d3a2f4c997c3462d1a03ebbd141eeeaec58da411e1cd62c1", "variant": "debug" }, "cpython-3.10.18+debug-linux-x86_64_v2-gnu": { @@ -18987,8 +19787,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.10.18%2B20250612-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "d93af68181616ae0f2403ac74d1cc2ea6ebced63394da5b3a3234398748ce4cf", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "72eacef9b8752ad823dbc009aa9be2b3199f7556218a63a4de0f1cb1b6bd08ec", "variant": "debug" }, "cpython-3.10.18+debug-linux-x86_64_v2-musl": { @@ -19003,8 +19803,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.10.18%2B20250612-x86_64_v2-unknown-linux-musl-debug-full.tar.zst", - "sha256": "af25b4aed5f7cdeb133c19c61f31038020315313eadbc4481493c8efce885194", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-x86_64_v2-unknown-linux-musl-debug-full.tar.zst", + "sha256": "01a9ddcf418a74805c17ceaa9ecdcbe0871f309e0649b43e6cd6f0fe883d8a14", "variant": "debug" }, "cpython-3.10.18+debug-linux-x86_64_v3-gnu": { @@ -19019,8 +19819,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.10.18%2B20250612-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "3da0ce9cd97415c86cdb8556e64883678e6b4684f74600b3bc9c90424db787af", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "06d979ecd8256d978f5a76f90063b198f08bb6833ead81e52a32f0213337f333", "variant": "debug" }, "cpython-3.10.18+debug-linux-x86_64_v3-musl": { @@ -19035,8 +19835,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.10.18%2B20250612-x86_64_v3-unknown-linux-musl-debug-full.tar.zst", - "sha256": "65fe2745075de7290c58b283af48acb6ab403396792a9682d24523bd025d7b01", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-x86_64_v3-unknown-linux-musl-debug-full.tar.zst", + "sha256": "f910de7dcd093e74d45b349bcd7a0cf291602979a90ef682fcf2b90a6bd4e301", "variant": "debug" }, "cpython-3.10.18+debug-linux-x86_64_v4-gnu": { @@ -19051,8 +19851,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.10.18%2B20250612-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "9b91c5309cbfee08636d405fce497b371e69787e9042be62dd8e262fc3800422", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "c3516587af46e3e38115b282f8585960e5050ad9a822e8d85c4159f1a209960f", "variant": "debug" }, "cpython-3.10.18+debug-linux-x86_64_v4-musl": { @@ -19067,8 +19867,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.10.18%2B20250612-x86_64_v4-unknown-linux-musl-debug-full.tar.zst", - "sha256": "aa146828d807b8c206541cc8b0bf2b5e7cecd1a9cf5f03b248e73b69b8ef5190", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-x86_64_v4-unknown-linux-musl-debug-full.tar.zst", + "sha256": "827cbcaad8cb5e16192f2f89f2e62a48ee3be89ec924d83e526f213806433d4a", "variant": "debug" }, "cpython-3.10.17-darwin-aarch64-none": { @@ -24011,8 +24811,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.9.23%2B20250612-aarch64-apple-darwin-install_only_stripped.tar.gz", - "sha256": "05741156232cc28facaefbda2d037605dd71614d343c7702e0d9ab52c156945e", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-aarch64-apple-darwin-install_only_stripped.tar.gz", + "sha256": "dbb161ced093b9b3b7f93ae7064fe20821471c2a4ac884e2bc94a216a2e19cba", "variant": null }, "cpython-3.9.23-darwin-x86_64-none": { @@ -24027,8 +24827,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.9.23%2B20250612-x86_64-apple-darwin-install_only_stripped.tar.gz", - "sha256": "3e6cf3c8c717f82f2b06442e0b78ececa7e7c67376262e48bf468b03e525ef31", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-x86_64-apple-darwin-install_only_stripped.tar.gz", + "sha256": "b7dbbec3f74512ce7282ffeb9d262c3605e824f564c49c698a83d8ad9a9810df", "variant": null }, "cpython-3.9.23-linux-aarch64-gnu": { @@ -24043,8 +24843,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.9.23%2B20250612-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "96ec244baeaf57a921da7797da41e49e9902a2a6b24c45072a8acee7ff9e881d", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "f8886fd05163ff181bc162399815e2c74b2dc85831a05ce76472fe10a0e509d6", "variant": null }, "cpython-3.9.23-linux-armv7-gnueabi": { @@ -24059,8 +24859,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.9.23%2B20250612-armv7-unknown-linux-gnueabi-install_only_stripped.tar.gz", - "sha256": "9c58d7126db2a66b81bff94b0e15a60974687f9ef74985d928e44397a10986cb", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-armv7-unknown-linux-gnueabi-install_only_stripped.tar.gz", + "sha256": "f05ac5a425eaf4ae61bfb8981627fb6b6a6224733d9bfbe79f1c9cd89694fa1a", "variant": null }, "cpython-3.9.23-linux-armv7-gnueabihf": { @@ -24075,8 +24875,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.9.23%2B20250612-armv7-unknown-linux-gnueabihf-install_only_stripped.tar.gz", - "sha256": "929fbed568da7325b7db351d32cd003ee77c4798f0f946a6840935054a07174f", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-armv7-unknown-linux-gnueabihf-install_only_stripped.tar.gz", + "sha256": "c5cb4da17943d74c1a94b6894d9960d96e8e4afc83e629a5788adbd2f8f4d51f", "variant": null }, "cpython-3.9.23-linux-powerpc64le-gnu": { @@ -24091,8 +24891,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.9.23%2B20250612-ppc64le-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "b52371b6485ab4603da77ff49b8092d470586a2330e56d58d9f8683a5590ae68", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-ppc64le-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "815e88f67598ebb039e1735a5d5f4f9facf61481111657a3ecefb69993a6a8ab", "variant": null }, "cpython-3.9.23-linux-riscv64-gnu": { @@ -24107,8 +24907,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.9.23%2B20250612-riscv64-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "ef4e83f827377bdb4656130ee953b442a33837ca31ef71547ac997d9979b91e4", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-riscv64-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "595c1ca3dbad46cc2370e17a5d415da82b75140a057449fc7de5d41faa2cc87a", "variant": null }, "cpython-3.9.23-linux-s390x-gnu": { @@ -24123,8 +24923,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.9.23%2B20250612-s390x-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "c8f5d2951900e41db551b692f2aa368e89449d3a4cf2761a80eb350fbd25bc0b", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-s390x-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "e6b2c9ca1962a069959f70d304e15ba0e63506db6e1b6bc3c75d03ca7012ac9f", "variant": null }, "cpython-3.9.23-linux-x86_64-gnu": { @@ -24139,8 +24939,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.9.23%2B20250612-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "461a5ef25341b2e9f62f21d3fa4184ac51af59cebb5fb9112fe64e338851109f", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "e6707a3a40fc68f37bad9b7ad9e018715af3be057b62bc80fee830a161c775f3", "variant": null }, "cpython-3.9.23-linux-x86_64-musl": { @@ -24155,8 +24955,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.9.23%2B20250612-x86_64-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "f5f3b7e7be74209fa76a9eed4645458e82bec3533d8aad1c45770506b1870571", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-x86_64-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "9ce6c722a5436f4adc1b25c01c5ada2c230f0089302a544d35b5b80702c0a7db", "variant": null }, "cpython-3.9.23-linux-x86_64_v2-gnu": { @@ -24171,8 +24971,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.9.23%2B20250612-x86_64_v2-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "302ae70b497361840e6f62e233f058ea0a41b91e4cc01050da940419b6b4b3d6", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-x86_64_v2-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "d6d6329699af5d463cfecef304e739347b1e485d959a63dc54b39a818dd0c5dd", "variant": null }, "cpython-3.9.23-linux-x86_64_v2-musl": { @@ -24187,8 +24987,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.9.23%2B20250612-x86_64_v2-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "740f1f6e1c1a70e7e7aaa47c0274ee6208b16bd1fe8d0c3600ecccb2481a5881", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-x86_64_v2-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "eb082839a9dd585e151395041f7832bb725a3bfc4e153e3f41949d43163902ab", "variant": null }, "cpython-3.9.23-linux-x86_64_v3-gnu": { @@ -24203,8 +25003,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.9.23%2B20250612-x86_64_v3-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "d4689ad72cb22973d12c4f7128588ed257e4976d69d92f361da4ddbcec8ce193", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-x86_64_v3-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "2a0080b853d55ae9db6b20209d63fbd4cacc794be647e7f9cf1a342dfd7e5796", "variant": null }, "cpython-3.9.23-linux-x86_64_v3-musl": { @@ -24219,8 +25019,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.9.23%2B20250612-x86_64_v3-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "5c094d6d91bf0e424c12b26627e0095f43879fefc8cf6050201b39b950163861", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-x86_64_v3-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "3eb47ce30946e30415538acf0e7a3efbac4679d5d16900c5c379f77ebef3321d", "variant": null }, "cpython-3.9.23-linux-x86_64_v4-gnu": { @@ -24235,8 +25035,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.9.23%2B20250612-x86_64_v4-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "2933a49ca7292e5ed1849aeb59057ec75ea9e018295d1bb8a3c155a7e46b3dde", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-x86_64_v4-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "2a81c218277a16515531df2568b8dc72cd1b2a2c22268882d57881a6f0f931d4", "variant": null }, "cpython-3.9.23-linux-x86_64_v4-musl": { @@ -24251,8 +25051,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.9.23%2B20250612-x86_64_v4-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "b8e92fe27a41d7d3751cdbffff7b4de3c84576fd960668555c20630d0860675e", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-x86_64_v4-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "a30191e1ecd58075909c5f28f21ddc290687afaa216b5cdd8c1f5a97963d1a95", "variant": null }, "cpython-3.9.23-windows-i686-none": { @@ -24267,8 +25067,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.9.23%2B20250612-i686-pc-windows-msvc-install_only_stripped.tar.gz", - "sha256": "d40486ddf29d6a70dda7ea8d56666733943de19ff8308b713175cba00d0f6a0f", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-i686-pc-windows-msvc-install_only_stripped.tar.gz", + "sha256": "f0482615c924623c3441ea96bfa1ea748b597db9099b8ad2e7c51526d799020b", "variant": null }, "cpython-3.9.23-windows-x86_64-none": { @@ -24283,8 +25083,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.9.23%2B20250612-x86_64-pc-windows-msvc-install_only_stripped.tar.gz", - "sha256": "5c4edcc6861f7fc72807c600666b060f3177a934577a0b1653f1ab511fdac4a0", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-x86_64-pc-windows-msvc-install_only_stripped.tar.gz", + "sha256": "d73d4aee5f2097cd1f5cc16a0f345c7cb53c3e8b94f1ca007b918a3152185c4b", "variant": null }, "cpython-3.9.23+debug-linux-aarch64-gnu": { @@ -24299,8 +25099,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.9.23%2B20250612-aarch64-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "c74a865c4e3848b38f4e1e96b24ba4e839c5776cb0ea72abe7b652a1524a1a51", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-aarch64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "140645dbd801afa90101806401cb2f855cd243e7b88a6c3bce665e39c252c6e1", "variant": "debug" }, "cpython-3.9.23+debug-linux-armv7-gnueabi": { @@ -24315,8 +25115,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.9.23%2B20250612-armv7-unknown-linux-gnueabi-debug-full.tar.zst", - "sha256": "5833d757964b5fe81f07183def651556ccd2c5cc9054373a6483b4ffb140ea72", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-armv7-unknown-linux-gnueabi-debug-full.tar.zst", + "sha256": "b6e8223142843640e8e5acbf96eaea72f2e7218e9da61de1d2215a626ebe207b", "variant": "debug" }, "cpython-3.9.23+debug-linux-armv7-gnueabihf": { @@ -24331,8 +25131,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.9.23%2B20250612-armv7-unknown-linux-gnueabihf-debug-full.tar.zst", - "sha256": "f5880944d32da9b4b65e88a21e4e2c3410ca299886a86db19566936b36fc445a", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-armv7-unknown-linux-gnueabihf-debug-full.tar.zst", + "sha256": "1b9cc881bcf52e9dd22da45782a11beda7de63e5d0644092412542ec8c3c2ce8", "variant": "debug" }, "cpython-3.9.23+debug-linux-powerpc64le-gnu": { @@ -24347,8 +25147,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.9.23%2B20250612-ppc64le-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "5ef02120a1e82c3d2c60f04380c8cac171cea59bc647e6089d4b2971e70a4b06", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-ppc64le-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "93cdb8c7bc5a13c273855b97e827a3b9b12522b7d7ed14e5110a7fa5043f7654", "variant": "debug" }, "cpython-3.9.23+debug-linux-riscv64-gnu": { @@ -24363,8 +25163,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.9.23%2B20250612-riscv64-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "b480c9dd67c1c2bccce609f145f562958e1235d294f8b5be385d3b5daca76e23", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-riscv64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "fb04b810e7980a211ab926f0db3430919790b86dee304682e2453669063fff34", "variant": "debug" }, "cpython-3.9.23+debug-linux-s390x-gnu": { @@ -24379,8 +25179,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.9.23%2B20250612-s390x-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "c2f3433e9820f2c8a70363d8faa7e881079e5b9e50a4764702c470add3c899ee", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-s390x-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "394cd53c4c012d7c32950600462a68fe0ed52f5204f745a7ebbc19f2473121b3", "variant": "debug" }, "cpython-3.9.23+debug-linux-x86_64-gnu": { @@ -24395,8 +25195,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.9.23%2B20250612-x86_64-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "120bc8aafdc5dcb828c27301776ccab4204feb3ad38fe03d7b0c8321617762f4", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-x86_64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "a772240194dd997da21e53029fde922da7e1893af1356eb47d710b2fbf144b0e", "variant": "debug" }, "cpython-3.9.23+debug-linux-x86_64-musl": { @@ -24411,8 +25211,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.9.23%2B20250612-x86_64-unknown-linux-musl-debug-full.tar.zst", - "sha256": "c79452c6ac6b7287b4119ba7a94cdaaa7edd50dbb489c76a2b3f1e3d0563b35a", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-x86_64-unknown-linux-musl-debug-full.tar.zst", + "sha256": "8153da9228087fc1ed655a83eb304318cc646333966ccb9ccd75ab9589b8585f", "variant": "debug" }, "cpython-3.9.23+debug-linux-x86_64_v2-gnu": { @@ -24427,8 +25227,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.9.23%2B20250612-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "75d93b474294a74cbe8b2445f5267299cf9db5e4fa0c6820408c5ac054549ff2", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "79d6c604c491c02cff2e1ffd632baf4d2418a85fe10dd1260ec860dde92322c1", "variant": "debug" }, "cpython-3.9.23+debug-linux-x86_64_v2-musl": { @@ -24443,8 +25243,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.9.23%2B20250612-x86_64_v2-unknown-linux-musl-debug-full.tar.zst", - "sha256": "a34fcea0dd98f275da0cb90e49df2d2bb6280fd010422fbe4f9234fabfc0f74d", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-x86_64_v2-unknown-linux-musl-debug-full.tar.zst", + "sha256": "7e222a1a6333e1e10bf476ac37ca2734885cb2cf92a7a8d3dc4f7fb81f69683b", "variant": "debug" }, "cpython-3.9.23+debug-linux-x86_64_v3-gnu": { @@ -24459,8 +25259,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.9.23%2B20250612-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "0415c91da8059adc423689961d5cf435c658efdca4f5a2036c65b9b7190ab26f", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "a03b38980fb6578be1a7c8e78f7219662e72ac1dc095b246d5a0b06d27e61ece", "variant": "debug" }, "cpython-3.9.23+debug-linux-x86_64_v3-musl": { @@ -24475,8 +25275,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.9.23%2B20250612-x86_64_v3-unknown-linux-musl-debug-full.tar.zst", - "sha256": "2bb65e171d6428d549162b5b305c8ab6e6877713a33009107d5f2934a13d547e", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-x86_64_v3-unknown-linux-musl-debug-full.tar.zst", + "sha256": "93df57f432ad2741e8edeefb1caf7a3d538f8c90cac732c6865d914d17577aed", "variant": "debug" }, "cpython-3.9.23+debug-linux-x86_64_v4-gnu": { @@ -24491,8 +25291,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.9.23%2B20250612-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "1b40bf44d9eddf38d2e0e3a20178358ece16fc940c5ee1e3cac15ae3d2d6c70e", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "2ba7ac2755c52728882cf187e8f2127b0b89cade6eaa2d42dd379d1bcd01a0a9", "variant": "debug" }, "cpython-3.9.23+debug-linux-x86_64_v4-musl": { @@ -24507,8 +25307,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.9.23%2B20250612-x86_64_v4-unknown-linux-musl-debug-full.tar.zst", - "sha256": "13b3c21d113b4e796dba7f52b93dfa97b7e658a910a90ab0433477db200c40ee", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-x86_64_v4-unknown-linux-musl-debug-full.tar.zst", + "sha256": "5253927e725888dae7a39caca1a88dcbfa002b2a115cb6e06000db04e133b51d", "variant": "debug" }, "cpython-3.9.22-darwin-aarch64-none": { diff --git a/crates/uv/tests/it/python_install.rs b/crates/uv/tests/it/python_install.rs index 817e71052..7fd596cd8 100644 --- a/crates/uv/tests/it/python_install.rs +++ b/crates/uv/tests/it/python_install.rs @@ -1444,8 +1444,8 @@ fn python_install_314() { ----- stdout ----- ----- stderr ----- - Installed Python 3.14.0b2 in [TIME] - + cpython-3.14.0b2-[PLATFORM] + Installed Python 3.14.0b3 in [TIME] + + cpython-3.14.0b3-[PLATFORM] "); // Install a specific pre-release @@ -1465,7 +1465,7 @@ fn python_install_314() { success: true exit_code: 0 ----- stdout ----- - [TEMP_DIR]/managed/cpython-3.14.0b2-[PLATFORM]/[INSTALL-BIN]/python + [TEMP_DIR]/managed/cpython-3.14.0b3-[PLATFORM]/[INSTALL-BIN]/python ----- stderr ----- "); @@ -1475,7 +1475,7 @@ fn python_install_314() { success: true exit_code: 0 ----- stdout ----- - [TEMP_DIR]/managed/cpython-3.14.0b2-[PLATFORM]/[INSTALL-BIN]/python + [TEMP_DIR]/managed/cpython-3.14.0b3-[PLATFORM]/[INSTALL-BIN]/python ----- stderr ----- "); @@ -1484,7 +1484,7 @@ fn python_install_314() { success: true exit_code: 0 ----- stdout ----- - [TEMP_DIR]/managed/cpython-3.14.0b2-[PLATFORM]/[INSTALL-BIN]/python + [TEMP_DIR]/managed/cpython-3.14.0b3-[PLATFORM]/[INSTALL-BIN]/python ----- stderr ----- "); From f892b8564fc3dc2e9c6db4453d8d8860c4a8ba65 Mon Sep 17 00:00:00 2001 From: John Mumm Date: Fri, 27 Jun 2025 19:54:52 +0200 Subject: [PATCH 107/185] Return `Cow` from `UrlString::with_` methods (#14319) A minor performance improvement as a follow-up to #14245 (and an accompanying test). --- crates/uv-distribution-types/src/file.rs | 53 ++++++++++++++---------- crates/uv-resolver/src/lock/mod.rs | 8 +++- 2 files changed, 37 insertions(+), 24 deletions(-) diff --git a/crates/uv-distribution-types/src/file.rs b/crates/uv-distribution-types/src/file.rs index 2d30eb0f2..948378c0c 100644 --- a/crates/uv-distribution-types/src/file.rs +++ b/crates/uv-distribution-types/src/file.rs @@ -1,3 +1,4 @@ +use std::borrow::Cow; use std::fmt::{self, Display, Formatter}; use std::str::FromStr; @@ -160,27 +161,22 @@ impl UrlString { .unwrap_or(self.as_ref()) } - /// Return the [`UrlString`] with any fragments removed. + /// Return the [`UrlString`] (as a [`Cow`]) with any fragments removed. #[must_use] - pub fn without_fragment(&self) -> Self { - Self( - self.as_ref() - .split_once('#') - .map(|(path, _)| path) - .map(SmallString::from) - .unwrap_or_else(|| self.0.clone()), - ) + pub fn without_fragment(&self) -> Cow<'_, Self> { + self.as_ref() + .split_once('#') + .map(|(path, _)| Cow::Owned(UrlString(SmallString::from(path)))) + .unwrap_or(Cow::Borrowed(self)) } - /// Return the [`UrlString`] with trailing slash removed. + /// Return the [`UrlString`] (as a [`Cow`]) with trailing slash removed. #[must_use] - pub fn without_trailing_slash(&self) -> Self { - Self( - self.as_ref() - .strip_suffix('/') - .map(SmallString::from) - .unwrap_or_else(|| self.0.clone()), - ) + pub fn without_trailing_slash(&self) -> Cow<'_, Self> { + self.as_ref() + .strip_suffix('/') + .map(|path| Cow::Owned(UrlString(SmallString::from(path)))) + .unwrap_or(Cow::Borrowed(self)) } } @@ -263,16 +259,29 @@ mod tests { #[test] fn without_fragment() { + // Borrows a URL without a fragment + let url = UrlString("https://example.com/path".into()); + assert_eq!(url.without_fragment(), Cow::Borrowed(&url)); + + // Removes the fragment if present on the URL let url = UrlString("https://example.com/path?query#fragment".into()); assert_eq!( url.without_fragment(), - UrlString("https://example.com/path?query".into()) + Cow::Owned(UrlString("https://example.com/path?query".into())) ); + } - let url = UrlString("https://example.com/path#fragment".into()); - assert_eq!(url.base_str(), "https://example.com/path"); - + #[test] + fn without_trailing_slash() { + // Borrows a URL without a slash let url = UrlString("https://example.com/path".into()); - assert_eq!(url.base_str(), "https://example.com/path"); + assert_eq!(url.without_trailing_slash(), Cow::Borrowed(&url)); + + // Removes the trailing slash if present on the URL + let url = UrlString("https://example.com/path/".into()); + assert_eq!( + url.without_trailing_slash(), + Cow::Owned(UrlString("https://example.com/path".into())) + ); } } diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index 8ff3097de..beeadc912 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -1490,7 +1490,11 @@ impl Lock { .version .as_ref() .expect("version for registry source"); - return Ok(SatisfiesResult::MissingRemoteIndex(name, version, url)); + return Ok(SatisfiesResult::MissingRemoteIndex( + name, + version, + url.into_owned(), + )); } } RegistrySource::Path(path) => { @@ -4692,7 +4696,7 @@ impl From for Hashes { /// Convert a [`FileLocation`] into a normalized [`UrlString`]. fn normalize_file_location(location: &FileLocation) -> Result { match location { - FileLocation::AbsoluteUrl(absolute) => Ok(absolute.without_fragment()), + FileLocation::AbsoluteUrl(absolute) => Ok(absolute.without_fragment().into_owned()), FileLocation::RelativeUrl(_, _) => Ok(normalize_url(location.to_url()?)), } } From 4eef79e5e83a0b980a0cd97781fbf4930dded582 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 27 Jun 2025 14:06:19 -0400 Subject: [PATCH 108/185] Avoid rendering desugared prefix matches in error messages (#14195) ## Summary When the user provides a requirement like `==2.4.*`, we desugar that to `>=2.4.dev0,<2.5.dev0`. These bounds then appear in error messages, and worse, they also trick the error message reporter into thinking that the user asked for a pre-release. This PR adds logic to convert to the more-concise `==2.4.*` representation when possible. We could probably do a similar thing for the compatible release operator (`~=`). Closes https://github.com/astral-sh/uv/issues/14177. Co-authored-by: Zanie Blue --- crates/uv-resolver/src/error.rs | 63 ++++++++++++++++++++++++ crates/uv-resolver/src/pubgrub/report.rs | 29 +++++++++-- crates/uv/tests/it/lock.rs | 28 +++++++++++ 3 files changed, 115 insertions(+), 5 deletions(-) diff --git a/crates/uv-resolver/src/error.rs b/crates/uv-resolver/src/error.rs index adbdc3cc7..2033ed0c0 100644 --- a/crates/uv-resolver/src/error.rs +++ b/crates/uv-resolver/src/error.rs @@ -1213,6 +1213,69 @@ impl SentinelRange<'_> { } } +/// A prefix match, e.g., `==2.4.*`, which is desugared to a range like `>=2.4.dev0,<2.5.dev0`. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct PrefixMatch<'a> { + version: &'a Version, +} + +impl<'a> PrefixMatch<'a> { + /// Determine whether a given range is equivalent to a prefix match (e.g., `==2.4.*`). + /// + /// Prefix matches are desugared to (e.g.) `>=2.4.dev0,<2.5.dev0`, but we want to render them + /// as `==2.4.*` in error messages. + pub(crate) fn from_range(lower: &'a Bound, upper: &'a Bound) -> Option { + let Bound::Included(lower) = lower else { + return None; + }; + let Bound::Excluded(upper) = upper else { + return None; + }; + if lower.is_pre() || lower.is_post() || lower.is_local() { + return None; + } + if upper.is_pre() || upper.is_post() || upper.is_local() { + return None; + } + if lower.dev() != Some(0) { + return None; + } + if upper.dev() != Some(0) { + return None; + } + if lower.release().len() != upper.release().len() { + return None; + } + + // All segments should be the same, except the last one, which should be incremented. + let num_segments = lower.release().len(); + for (i, (lower, upper)) in lower + .release() + .iter() + .zip(upper.release().iter()) + .enumerate() + { + if i == num_segments - 1 { + if lower + 1 != *upper { + return None; + } + } else { + if lower != upper { + return None; + } + } + } + + Some(PrefixMatch { version: lower }) + } +} + +impl std::fmt::Display for PrefixMatch<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "=={}.*", self.version.only_release()) + } +} + #[derive(Debug)] pub struct NoSolutionHeader { /// The [`ResolverEnvironment`] that caused the failure. diff --git a/crates/uv-resolver/src/pubgrub/report.rs b/crates/uv-resolver/src/pubgrub/report.rs index 91f8d4baa..5c62f0b1f 100644 --- a/crates/uv-resolver/src/pubgrub/report.rs +++ b/crates/uv-resolver/src/pubgrub/report.rs @@ -18,7 +18,7 @@ use uv_pep440::{Version, VersionSpecifiers}; use uv_platform_tags::{AbiTag, IncompatibleTag, LanguageTag, PlatformTag, Tags}; use crate::candidate_selector::CandidateSelector; -use crate::error::ErrorTree; +use crate::error::{ErrorTree, PrefixMatch}; use crate::fork_indexes::ForkIndexes; use crate::fork_urls::ForkUrls; use crate::prerelease::AllowPrerelease; @@ -944,17 +944,30 @@ impl PubGrubReportFormatter<'_> { hints: &mut IndexSet, ) { let any_prerelease = set.iter().any(|(start, end)| { + // Ignore, e.g., `>=2.4.dev0,<2.5.dev0`, which is the desugared form of `==2.4.*`. + if PrefixMatch::from_range(start, end).is_some() { + return false; + } + let is_pre1 = match start { Bound::Included(version) => version.any_prerelease(), Bound::Excluded(version) => version.any_prerelease(), Bound::Unbounded => false, }; + if is_pre1 { + return true; + } + let is_pre2 = match end { Bound::Included(version) => version.any_prerelease(), Bound::Excluded(version) => version.any_prerelease(), Bound::Unbounded => false, }; - is_pre1 || is_pre2 + if is_pre2 { + return true; + } + + false }); if any_prerelease { @@ -1928,11 +1941,11 @@ impl std::fmt::Display for PackageRange<'_> { PackageRangeKind::Available => write!(f, "are available:")?, } } - for segment in &segments { + for (lower, upper) in &segments { if segments.len() > 1 { write!(f, "\n ")?; } - match segment { + match (lower, upper) { (Bound::Unbounded, Bound::Unbounded) => match self.kind { PackageRangeKind::Dependency => write!(f, "{package}")?, PackageRangeKind::Compatibility => write!(f, "all versions of {package}")?, @@ -1948,7 +1961,13 @@ impl std::fmt::Display for PackageRange<'_> { write!(f, "{package}>={v},<={b}")?; } } - (Bound::Included(v), Bound::Excluded(b)) => write!(f, "{package}>={v},<{b}")?, + (Bound::Included(v), Bound::Excluded(b)) => { + if let Some(prefix) = PrefixMatch::from_range(lower, upper) { + write!(f, "{package}{prefix}")?; + } else { + write!(f, "{package}>={v},<{b}")?; + } + } (Bound::Excluded(v), Bound::Unbounded) => write!(f, "{package}>{v}")?, (Bound::Excluded(v), Bound::Included(b)) => write!(f, "{package}>{v},<={b}")?, (Bound::Excluded(v), Bound::Excluded(b)) => write!(f, "{package}>{v},<{b}")?, diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index e0fc8749d..b1d6c5327 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -28452,3 +28452,31 @@ fn lock_with_index_trailing_slashes_in_lockfile() -> Result<()> { Ok(()) } + +#[test] +fn lock_prefix_match() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["anyio==5.4.*"] + "#, + )?; + + uv_snapshot!(context.filters(), context.lock(), @r" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because only anyio<=4.3.0 is available and your project depends on anyio==5.4.*, we can conclude that your project's requirements are unsatisfiable. + "); + + Ok(()) +} From 6a5d2f1ec4b8a43a38fb29cf31d57fd9f2bc362f Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 27 Jun 2025 14:48:40 -0400 Subject: [PATCH 109/185] Share workspace cache between lock and sync operations (#14321) ## Summary Closes #14316. --- crates/uv/src/commands/project/add.rs | 3 +++ crates/uv/src/commands/project/export.rs | 1 + crates/uv/src/commands/project/lock.rs | 11 ++++++++--- crates/uv/src/commands/project/remove.rs | 2 ++ crates/uv/src/commands/project/run.rs | 7 +++++-- crates/uv/src/commands/project/sync.rs | 7 +++++-- crates/uv/src/commands/project/tree.rs | 1 + crates/uv/src/commands/project/version.rs | 4 ++++ 8 files changed, 29 insertions(+), 7 deletions(-) diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index ae20b31d4..bd3b49a3f 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -853,6 +853,7 @@ async fn lock_and_sync( Box::new(DefaultResolveLogger), concurrency, cache, + &WorkspaceCache::default(), printer, preview, ) @@ -974,6 +975,7 @@ async fn lock_and_sync( Box::new(SummaryResolveLogger), concurrency, cache, + &WorkspaceCache::default(), printer, preview, ) @@ -1021,6 +1023,7 @@ async fn lock_and_sync( installer_metadata, concurrency, cache, + WorkspaceCache::default(), DryRun::Disabled, printer, preview, diff --git a/crates/uv/src/commands/project/export.rs b/crates/uv/src/commands/project/export.rs index 88a847d04..c14bfd904 100644 --- a/crates/uv/src/commands/project/export.rs +++ b/crates/uv/src/commands/project/export.rs @@ -193,6 +193,7 @@ pub(crate) async fn export( Box::new(DefaultResolveLogger), concurrency, cache, + &workspace_cache, printer, preview, ) diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index 97ee01767..2bcb68eb3 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -199,6 +199,7 @@ pub(crate) async fn lock( Box::new(DefaultResolveLogger), concurrency, cache, + &workspace_cache, printer, preview, ) @@ -263,6 +264,7 @@ pub(super) struct LockOperation<'env> { logger: Box, concurrency: Concurrency, cache: &'env Cache, + workspace_cache: &'env WorkspaceCache, printer: Printer, preview: PreviewMode, } @@ -277,6 +279,7 @@ impl<'env> LockOperation<'env> { logger: Box, concurrency: Concurrency, cache: &'env Cache, + workspace_cache: &'env WorkspaceCache, printer: Printer, preview: PreviewMode, ) -> Self { @@ -289,6 +292,7 @@ impl<'env> LockOperation<'env> { logger, concurrency, cache, + workspace_cache, printer, preview, } @@ -334,6 +338,7 @@ impl<'env> LockOperation<'env> { self.logger, self.concurrency, self.cache, + self.workspace_cache, self.printer, self.preview, ) @@ -372,6 +377,7 @@ impl<'env> LockOperation<'env> { self.logger, self.concurrency, self.cache, + self.workspace_cache, self.printer, self.preview, ) @@ -402,6 +408,7 @@ async fn do_lock( logger: Box, concurrency: Concurrency, cache: &Cache, + workspace_cache: &WorkspaceCache, printer: Printer, preview: PreviewMode, ) -> Result { @@ -654,8 +661,6 @@ async fn do_lock( FlatIndex::from_entries(entries, None, &hasher, build_options) }; - let workspace_cache = WorkspaceCache::default(); - // Create a build dispatch. let build_dispatch = BuildDispatch::new( &client, @@ -674,7 +679,7 @@ async fn do_lock( &build_hasher, *exclude_newer, *sources, - workspace_cache, + workspace_cache.clone(), concurrency, preview, ); diff --git a/crates/uv/src/commands/project/remove.rs b/crates/uv/src/commands/project/remove.rs index 7fd02277e..29b5f0bc0 100644 --- a/crates/uv/src/commands/project/remove.rs +++ b/crates/uv/src/commands/project/remove.rs @@ -302,6 +302,7 @@ pub(crate) async fn remove( Box::new(DefaultResolveLogger), concurrency, cache, + &WorkspaceCache::default(), printer, preview, ) @@ -357,6 +358,7 @@ pub(crate) async fn remove( installer_metadata, concurrency, cache, + WorkspaceCache::default(), DryRun::Disabled, printer, preview, diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index ee3caeafa..6ece28eaf 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -264,6 +264,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl }, concurrency, cache, + &workspace_cache, printer, preview, ) @@ -309,6 +310,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl installer_metadata, concurrency, cache, + workspace_cache.clone(), DryRun::Disabled, printer, preview, @@ -407,7 +409,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl installer_metadata, concurrency, cache, - workspace_cache, + workspace_cache.clone(), DryRun::Disabled, printer, preview, @@ -465,7 +467,6 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl }; // Discover and sync the base environment. - let workspace_cache = WorkspaceCache::default(); let temp_dir; let base_interpreter = if let Some(script_interpreter) = script_interpreter { // If we found a PEP 723 script and the user provided a project-only setting, warn. @@ -721,6 +722,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl }, concurrency, cache, + &workspace_cache, printer, preview, ) @@ -807,6 +809,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl installer_metadata, concurrency, cache, + workspace_cache.clone(), DryRun::Disabled, printer, preview, diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index f1a73b8c8..19848ee02 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -324,7 +324,7 @@ pub(crate) async fn sync( installer_metadata, concurrency, cache, - workspace_cache, + workspace_cache.clone(), dry_run, printer, preview, @@ -371,6 +371,7 @@ pub(crate) async fn sync( Box::new(DefaultResolveLogger), concurrency, cache, + &workspace_cache, printer, preview, ) @@ -452,6 +453,7 @@ pub(crate) async fn sync( installer_metadata, concurrency, cache, + workspace_cache, dry_run, printer, preview, @@ -587,6 +589,7 @@ pub(super) async fn do_sync( installer_metadata: bool, concurrency: Concurrency, cache: &Cache, + workspace_cache: WorkspaceCache, dry_run: DryRun, printer: Printer, preview: PreviewMode, @@ -748,7 +751,7 @@ pub(super) async fn do_sync( &build_hasher, exclude_newer, sources, - WorkspaceCache::default(), + workspace_cache.clone(), concurrency, preview, ); diff --git a/crates/uv/src/commands/project/tree.rs b/crates/uv/src/commands/project/tree.rs index 2ff6ad98e..d401940d9 100644 --- a/crates/uv/src/commands/project/tree.rs +++ b/crates/uv/src/commands/project/tree.rs @@ -146,6 +146,7 @@ pub(crate) async fn tree( Box::new(DefaultResolveLogger), concurrency, cache, + &WorkspaceCache::default(), printer, preview, ) diff --git a/crates/uv/src/commands/project/version.rs b/crates/uv/src/commands/project/version.rs index fdba41978..102d91af6 100644 --- a/crates/uv/src/commands/project/version.rs +++ b/crates/uv/src/commands/project/version.rs @@ -315,6 +315,7 @@ async fn print_frozen_version( Box::new(DefaultResolveLogger), concurrency, cache, + &WorkspaceCache::default(), printer, preview, ) @@ -443,6 +444,7 @@ async fn lock_and_sync( // Initialize any shared state. let state = UniversalState::default(); + let workspace_cache = WorkspaceCache::default(); // Lock and sync the environment, if necessary. let lock = match project::lock::LockOperation::new( @@ -453,6 +455,7 @@ async fn lock_and_sync( Box::new(DefaultResolveLogger), concurrency, cache, + &workspace_cache, printer, preview, ) @@ -510,6 +513,7 @@ async fn lock_and_sync( installer_metadata, concurrency, cache, + workspace_cache, DryRun::Disabled, printer, preview, From eab938b7b4a9ce49e1eb5ffdcfec799e5538ccd8 Mon Sep 17 00:00:00 2001 From: Aaron Ang <67321817+aaron-ang@users.noreply.github.com> Date: Fri, 27 Jun 2025 13:48:41 -0700 Subject: [PATCH 110/185] Warn users on `~=` python version specifier (#14008) Close #7426 ## Summary Picking up on #8284, I noticed that the `requires_python` object already has its specifiers canonicalized in the `intersection` method, meaning `~=3.12` is converted to `>=3.12, <4`. To fix this, we check and warn in `intersection`. ## Test Plan Used the same tests from #8284. --- .../src/requires_python.rs | 32 +++++++--- crates/uv-pep440/src/version_ranges.rs | 9 ++- crates/uv-pep440/src/version_specifier.rs | 7 +- crates/uv/tests/it/lock.rs | 64 +++++++++++++++++++ 4 files changed, 97 insertions(+), 15 deletions(-) diff --git a/crates/uv-distribution-types/src/requires_python.rs b/crates/uv-distribution-types/src/requires_python.rs index ae9fee7fe..786aed83a 100644 --- a/crates/uv-distribution-types/src/requires_python.rs +++ b/crates/uv-distribution-types/src/requires_python.rs @@ -5,10 +5,11 @@ use version_ranges::Ranges; use uv_distribution_filename::WheelFilename; use uv_pep440::{ LowerBound, UpperBound, Version, VersionSpecifier, VersionSpecifiers, - release_specifiers_to_ranges, + release_specifier_to_range, release_specifiers_to_ranges, }; use uv_pep508::{MarkerExpression, MarkerTree, MarkerValueVersion}; use uv_platform_tags::{AbiTag, LanguageTag}; +use uv_warnings::warn_user_once; /// The `Requires-Python` requirement specifier. /// @@ -66,15 +67,28 @@ impl RequiresPython { ) -> Option { // Convert to PubGrub range and perform an intersection. let range = specifiers - .into_iter() - .map(|specifier| release_specifiers_to_ranges(specifier.clone())) - .fold(None, |range: Option>, requires_python| { - if let Some(range) = range { - Some(range.intersection(&requires_python)) - } else { - Some(requires_python) + .map(|specs| { + // Warn if there’s exactly one `~=` specifier without a patch. + if let [spec] = &specs[..] { + if spec.is_tilde_without_patch() { + if let Some((lo_b, hi_b)) = release_specifier_to_range(spec.clone()) + .bounding_range() + .map(|(l, u)| (l.cloned(), u.cloned())) + { + let lo_spec = LowerBound::new(lo_b).specifier().unwrap(); + let hi_spec = UpperBound::new(hi_b).specifier().unwrap(); + warn_user_once!( + "The release specifier (`{spec}`) contains a compatible release \ + match without a patch version. This will be interpreted as \ + `{lo_spec}, {hi_spec}`. Did you mean `{spec}.0` to freeze the minor \ + version?" + ); + } + } } - })?; + release_specifiers_to_ranges(specs.clone()) + }) + .reduce(|acc, r| acc.intersection(&r))?; // If the intersection is empty, return `None`. if range.is_empty() { diff --git a/crates/uv-pep440/src/version_ranges.rs b/crates/uv-pep440/src/version_ranges.rs index 2bd7dcd4d..26cd048d3 100644 --- a/crates/uv-pep440/src/version_ranges.rs +++ b/crates/uv-pep440/src/version_ranges.rs @@ -130,11 +130,10 @@ impl From for Ranges { /// /// See: pub fn release_specifiers_to_ranges(specifiers: VersionSpecifiers) -> Ranges { - let mut range = Ranges::full(); - for specifier in specifiers { - range = range.intersection(&release_specifier_to_range(specifier)); - } - range + specifiers + .into_iter() + .map(release_specifier_to_range) + .fold(Ranges::full(), |acc, range| acc.intersection(&range)) } /// Convert the [`VersionSpecifier`] to a PubGrub-compatible version range, using release-only diff --git a/crates/uv-pep440/src/version_specifier.rs b/crates/uv-pep440/src/version_specifier.rs index 4255c13fa..19acff2eb 100644 --- a/crates/uv-pep440/src/version_specifier.rs +++ b/crates/uv-pep440/src/version_specifier.rs @@ -416,7 +416,7 @@ impl VersionSpecifier { &self.operator } - /// Get the version, e.g. `<=` in `<= 2.0.0` + /// Get the version, e.g. `2.0.0` in `<= 2.0.0` pub fn version(&self) -> &Version { &self.version } @@ -615,6 +615,11 @@ impl VersionSpecifier { | Operator::NotEqual => false, } } + + /// Returns true if this is a `~=` specifier without a patch version (e.g. `~=3.11`). + pub fn is_tilde_without_patch(&self) -> bool { + self.operator == Operator::TildeEqual && self.version.release().len() == 2 + } } impl FromStr for VersionSpecifier { diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index b1d6c5327..5fb0fcd27 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -4535,6 +4535,70 @@ fn lock_requires_python_exact() -> Result<()> { Ok(()) } +/// Lock a requirement from PyPI with a compatible release Python bound. +#[cfg(feature = "python-patch")] +#[test] +fn lock_requires_python_compatible_specifier() -> Result<()> { + let context = TestContext::new("3.13.0"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "warehouse" + version = "1.0.0" + requires-python = "~=3.13" + "#, + )?; + + uv_snapshot!(context.filters(), context.lock(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: The release specifier (`~=3.13`) contains a compatible release match without a patch version. This will be interpreted as `>=3.13, <4`. Did you mean `~=3.13.0` to freeze the minor version? + Resolved 1 package in [TIME] + "###); + + pyproject_toml.write_str( + r#" + [project] + name = "warehouse" + version = "1.0.0" + requires-python = "~=3.13, <3.14" + "#, + )?; + + uv_snapshot!(context.filters(), context.lock(), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + "); + + pyproject_toml.write_str( + r#" + [project] + name = "warehouse" + version = "1.0.0" + requires-python = "~=3.13.0" + "#, + )?; + + uv_snapshot!(context.filters(), context.lock(), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + "); + Ok(()) +} + /// Fork, even with a single dependency, if the minimum Python version is increased. #[test] fn lock_requires_python_fork() -> Result<()> { From b6b7409d13e53b7f0752dc053bce9bd3b233269b Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Fri, 27 Jun 2025 16:46:36 -0500 Subject: [PATCH 111/185] Bump version to 0.7.16 (#14334) --- CHANGELOG.md | 82 +++++++++++++++++++-------- Cargo.lock | 6 +- crates/uv-build/Cargo.toml | 2 +- crates/uv-build/pyproject.toml | 2 +- crates/uv-version/Cargo.toml | 2 +- crates/uv/Cargo.toml | 2 +- docs/concepts/build-backend.md | 2 +- docs/getting-started/installation.md | 4 +- docs/guides/integration/aws-lambda.md | 4 +- docs/guides/integration/docker.md | 10 ++-- docs/guides/integration/github.md | 2 +- docs/guides/integration/pre-commit.md | 10 ++-- pyproject.toml | 2 +- 13 files changed, 83 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ef35e9a3..f2d261a73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,42 @@ +## 0.7.16 + +### Python + +- Add Python 3.14.0b3 + +See the +[`python-build-standalone` release notes](https://github.com/astral-sh/python-build-standalone/releases/tag/20250626) +for more details. + +### Enhancements + +- Include path or URL when failing to convert in lockfile ([#14292](https://github.com/astral-sh/uv/pull/14292)) +- Warn when `~=` is used as a Python version specifier without a patch version ([#14008](https://github.com/astral-sh/uv/pull/14008)) + +### Preview features + +- Ensure preview default Python installs are upgradeable ([#14261](https://github.com/astral-sh/uv/pull/14261)) + +### Performance + +- Share workspace cache between lock and sync operations ([#14321](https://github.com/astral-sh/uv/pull/14321)) + +### Bug fixes + +- Allow local indexes to reference remote files ([#14294](https://github.com/astral-sh/uv/pull/14294)) +- Avoid rendering desugared prefix matches in error messages ([#14195](https://github.com/astral-sh/uv/pull/14195)) +- Avoid using path URL for workspace Git dependencies in `requirements.txt` ([#14288](https://github.com/astral-sh/uv/pull/14288)) +- Normalize index URLs to remove trailing slash ([#14245](https://github.com/astral-sh/uv/pull/14245)) +- Respect URL-encoded credentials in redirect location ([#14315](https://github.com/astral-sh/uv/pull/14315)) +- Lock the source tree when running setuptools, to protect concurrent builds ([#14174](https://github.com/astral-sh/uv/pull/14174)) + +### Documentation + +- Note that GCP Artifact Registry download URLs must have `/simple` component ([#14251](https://github.com/astral-sh/uv/pull/14251)) + ## 0.7.15 ### Enhancements @@ -408,11 +444,11 @@ This release contains various changes that improve correctness and user experien ### Breaking changes - **Update `uv version` to display and update project versions ([#12349](https://github.com/astral-sh/uv/pull/12349))** - + Previously, `uv version` displayed uv's version. Now, `uv version` will display or update the project's version. This interface was [heavily requested](https://github.com/astral-sh/uv/issues/6298) and, after much consideration, we decided that transitioning the top-level command was the best option. - + Here's a brief example: - + ```console $ uv init example Initialized project `example` at `./example` @@ -424,72 +460,72 @@ This release contains various changes that improve correctness and user experien $ uv version --short 1.0.0 ``` - + If used outside of a project, uv will fallback to showing its own version still: - + ```console $ uv version warning: failed to read project: No `pyproject.toml` found in current directory or any parent directory running `uv self version` for compatibility with old `uv version` command. this fallback will be removed soon, pass `--preview` to make this an error. - + uv 0.7.0 (4433f41c9 2025-04-29) ``` - + As described in the warning, `--preview` can be used to error instead: - + ```console $ uv version --preview error: No `pyproject.toml` found in current directory or any parent directory ``` - + The previous functionality of `uv version` was moved to `uv self version`. - **Avoid fallback to subsequent indexes on authentication failure ([#12805](https://github.com/astral-sh/uv/pull/12805))** - + When using the `first-index` strategy (the default), uv will stop searching indexes for a package once it is found on a single index. Previously, uv considered a package as "missing" from an index during authentication failures, such as an HTTP 401 or HTTP 403 (normally, missing packages are represented by an HTTP 404). This behavior was motivated by unusual responses from some package indexes, but reduces the safety of uv's index strategy when authentication fails. Now, uv will consider an authentication failure as a stop-point when searching for a package across indexes. The `index.ignore-error-codes` option can be used to recover the existing behavior, e.g.: - + ```toml [[tool.uv.index]] name = "pytorch" url = "https://download.pytorch.org/whl/cpu" ignore-error-codes = [401, 403] ``` - + Since PyTorch's indexes always return a HTTP 403 for missing packages, uv special-cases indexes on the `pytorch.org` domain to ignore that error code by default. - **Require the command in `uvx ` to be available in the Python environment ([#11603](https://github.com/astral-sh/uv/pull/11603))** - + Previously, `uvx` would attempt to execute a command even if it was not provided by a Python package. For example, if we presume `foo` is an empty Python package which provides no command, `uvx foo` would invoke the `foo` command on the `PATH` (if present). Now, uv will error early if the `foo` executable is not provided by the requested Python package. This check is not enforced when `--from` is used, so patterns like `uvx --from foo bash -c "..."` are still valid. uv also still allows `uvx foo` where the `foo` executable is provided by a dependency of `foo` instead of `foo` itself, as this is fairly common for packages which depend on a dedicated package for their command-line interface. - **Use index URL instead of package URL for keyring credential lookups ([#12651](https://github.com/astral-sh/uv/pull/12651))** - + When determining credentials for querying a package URL, uv previously sent the full URL to the `keyring` command. However, some keyring plugins expect to receive the *index URL* (which is usually a parent of the package URL). Now, uv requests credentials for the index URL instead. This behavior matches `pip`. - **Remove `--version` from subcommands ([#13108](https://github.com/astral-sh/uv/pull/13108))** - + Previously, uv allowed the `--version` flag on arbitrary subcommands, e.g., `uv run --version`. However, the `--version` flag is useful for other operations since uv is a package manager. Consequently, we've removed the `--version` flag from subcommands — it is only available as `uv --version`. - **Omit Python 3.7 downloads from managed versions ([#13022](https://github.com/astral-sh/uv/pull/13022))** - + Python 3.7 is EOL and not formally supported by uv; however, Python 3.7 was previously available for download on a subset of platforms. - **Reject non-PEP 751 TOML files in install, compile, and export commands ([#13120](https://github.com/astral-sh/uv/pull/13120), [#13119](https://github.com/astral-sh/uv/pull/13119))** - + Previously, uv treated arbitrary `.toml` files passed to commands (e.g., `uv pip install -r foo.toml` or `uv pip compile -o foo.toml`) as `requirements.txt`-formatted files. Now, uv will error instead. If using PEP 751 lockfiles, use the standardized format for custom names instead, e.g., `pylock.foo.toml`. - **Ignore arbitrary Python requests in version files ([#12909](https://github.com/astral-sh/uv/pull/12909))** - + uv allows arbitrary strings to be used for Python version requests, in which they are treated as an executable name to search for in the `PATH`. However, using this form of request in `.python-version` files is non-standard and conflicts with `pyenv-virtualenv` which writes environment names to `.python-version` files. In this release, uv will now ignore requests that are arbitrary strings when found in `.python-version` files. - **Error on unknown dependency object specifiers ([12811](https://github.com/astral-sh/uv/pull/12811))** - + The `[dependency-groups]` entries can include "object specifiers", e.g. `set-phasers-to = ...` in: - + ```toml [dependency-groups] foo = ["pyparsing"] bar = [{set-phasers-to = "stun"}] ``` - + However, the only current spec-compliant object specifier is `include-group`. Previously, uv would ignore unknown object specifiers. Now, uv will error. - **Make `--frozen` and `--no-sources` conflicting options ([#12671](https://github.com/astral-sh/uv/pull/12671))** - + Using `--no-sources` always requires a new resolution and `--frozen` will always fail when used with it. Now, this conflict is encoded in the CLI options for clarity. - **Treat empty `UV_PYTHON_INSTALL_DIR` and `UV_TOOL_DIR` as unset ([#12907](https://github.com/astral-sh/uv/pull/12907), [#12905](https://github.com/astral-sh/uv/pull/12905))** - + Previously, these variables were treated as set to the current working directory when set to an empty string. Now, uv will ignore these variables when empty. This matches uv's behavior for other environment variables which configure directories. ### Enhancements diff --git a/Cargo.lock b/Cargo.lock index d25d7d252..ef9f5b97a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4570,7 +4570,7 @@ dependencies = [ [[package]] name = "uv" -version = "0.7.15" +version = "0.7.16" dependencies = [ "anstream", "anyhow", @@ -4734,7 +4734,7 @@ dependencies = [ [[package]] name = "uv-build" -version = "0.7.15" +version = "0.7.16" dependencies = [ "anyhow", "uv-build-backend", @@ -5923,7 +5923,7 @@ dependencies = [ [[package]] name = "uv-version" -version = "0.7.15" +version = "0.7.16" [[package]] name = "uv-virtualenv" diff --git a/crates/uv-build/Cargo.toml b/crates/uv-build/Cargo.toml index 94b4adbc3..6c3ab09d9 100644 --- a/crates/uv-build/Cargo.toml +++ b/crates/uv-build/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-build" -version = "0.7.15" +version = "0.7.16" edition.workspace = true rust-version.workspace = true homepage.workspace = true diff --git a/crates/uv-build/pyproject.toml b/crates/uv-build/pyproject.toml index 4b8c522d6..5687497ae 100644 --- a/crates/uv-build/pyproject.toml +++ b/crates/uv-build/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uv-build" -version = "0.7.15" +version = "0.7.16" description = "The uv build backend" authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }] requires-python = ">=3.8" diff --git a/crates/uv-version/Cargo.toml b/crates/uv-version/Cargo.toml index 9eef680a2..5d3be8a9d 100644 --- a/crates/uv-version/Cargo.toml +++ b/crates/uv-version/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-version" -version = "0.7.15" +version = "0.7.16" edition = { workspace = true } rust-version = { workspace = true } homepage = { workspace = true } diff --git a/crates/uv/Cargo.toml b/crates/uv/Cargo.toml index 9b73a9ebd..5f93c3ef6 100644 --- a/crates/uv/Cargo.toml +++ b/crates/uv/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv" -version = "0.7.15" +version = "0.7.16" edition = { workspace = true } rust-version = { workspace = true } homepage = { workspace = true } diff --git a/docs/concepts/build-backend.md b/docs/concepts/build-backend.md index 2e93cc546..260680d4c 100644 --- a/docs/concepts/build-backend.md +++ b/docs/concepts/build-backend.md @@ -19,7 +19,7 @@ existing project, add it to the `[build-system]` section in your `pyproject.toml ```toml [build-system] -requires = ["uv_build>=0.7.15,<0.8.0"] +requires = ["uv_build>=0.7.16,<0.8.0"] build-backend = "uv_build" ``` diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 33281a0c8..d2235fc71 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -25,7 +25,7 @@ uv provides a standalone installer to download and install uv: Request a specific version by including it in the URL: ```console - $ curl -LsSf https://astral.sh/uv/0.7.15/install.sh | sh + $ curl -LsSf https://astral.sh/uv/0.7.16/install.sh | sh ``` === "Windows" @@ -41,7 +41,7 @@ uv provides a standalone installer to download and install uv: Request a specific version by including it in the URL: ```pwsh-session - PS> powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/0.7.15/install.ps1 | iex" + PS> powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/0.7.16/install.ps1 | iex" ``` !!! tip diff --git a/docs/guides/integration/aws-lambda.md b/docs/guides/integration/aws-lambda.md index b48dba1b1..25be2ae38 100644 --- a/docs/guides/integration/aws-lambda.md +++ b/docs/guides/integration/aws-lambda.md @@ -92,7 +92,7 @@ the second stage, we'll copy this directory over to the final image, omitting th other unnecessary files. ```dockerfile title="Dockerfile" -FROM ghcr.io/astral-sh/uv:0.7.15 AS uv +FROM ghcr.io/astral-sh/uv:0.7.16 AS uv # First, bundle the dependencies into the task root. FROM public.ecr.aws/lambda/python:3.13 AS builder @@ -334,7 +334,7 @@ And confirm that opening http://127.0.0.1:8000/ in a web browser displays, "Hell Finally, we'll update the Dockerfile to include the local library in the deployment package: ```dockerfile title="Dockerfile" -FROM ghcr.io/astral-sh/uv:0.7.15 AS uv +FROM ghcr.io/astral-sh/uv:0.7.16 AS uv # First, bundle the dependencies into the task root. FROM public.ecr.aws/lambda/python:3.13 AS builder diff --git a/docs/guides/integration/docker.md b/docs/guides/integration/docker.md index d8e6d69a3..72d730362 100644 --- a/docs/guides/integration/docker.md +++ b/docs/guides/integration/docker.md @@ -31,7 +31,7 @@ $ docker run --rm -it ghcr.io/astral-sh/uv:debian uv --help The following distroless images are available: - `ghcr.io/astral-sh/uv:latest` -- `ghcr.io/astral-sh/uv:{major}.{minor}.{patch}`, e.g., `ghcr.io/astral-sh/uv:0.7.15` +- `ghcr.io/astral-sh/uv:{major}.{minor}.{patch}`, e.g., `ghcr.io/astral-sh/uv:0.7.16` - `ghcr.io/astral-sh/uv:{major}.{minor}`, e.g., `ghcr.io/astral-sh/uv:0.7` (the latest patch version) @@ -75,7 +75,7 @@ And the following derived images are available: As with the distroless image, each derived image is published with uv version tags as `ghcr.io/astral-sh/uv:{major}.{minor}.{patch}-{base}` and -`ghcr.io/astral-sh/uv:{major}.{minor}-{base}`, e.g., `ghcr.io/astral-sh/uv:0.7.15-alpine`. +`ghcr.io/astral-sh/uv:{major}.{minor}-{base}`, e.g., `ghcr.io/astral-sh/uv:0.7.16-alpine`. For more details, see the [GitHub Container](https://github.com/astral-sh/uv/pkgs/container/uv) page. @@ -113,7 +113,7 @@ Note this requires `curl` to be available. In either case, it is best practice to pin to a specific uv version, e.g., with: ```dockerfile -COPY --from=ghcr.io/astral-sh/uv:0.7.15 /uv /uvx /bin/ +COPY --from=ghcr.io/astral-sh/uv:0.7.16 /uv /uvx /bin/ ``` !!! tip @@ -131,7 +131,7 @@ COPY --from=ghcr.io/astral-sh/uv:0.7.15 /uv /uvx /bin/ Or, with the installer: ```dockerfile -ADD https://astral.sh/uv/0.7.15/install.sh /uv-installer.sh +ADD https://astral.sh/uv/0.7.16/install.sh /uv-installer.sh ``` ### Installing a project @@ -557,5 +557,5 @@ Verified OK !!! tip These examples use `latest`, but best practice is to verify the attestation for a specific - version tag, e.g., `ghcr.io/astral-sh/uv:0.7.15`, or (even better) the specific image digest, + version tag, e.g., `ghcr.io/astral-sh/uv:0.7.16`, or (even better) the specific image digest, such as `ghcr.io/astral-sh/uv:0.5.27@sha256:5adf09a5a526f380237408032a9308000d14d5947eafa687ad6c6a2476787b4f`. diff --git a/docs/guides/integration/github.md b/docs/guides/integration/github.md index ea96199a2..56fc51e21 100644 --- a/docs/guides/integration/github.md +++ b/docs/guides/integration/github.md @@ -47,7 +47,7 @@ jobs: uses: astral-sh/setup-uv@v5 with: # Install a specific version of uv. - version: "0.7.15" + version: "0.7.16" ``` ## Setting up Python diff --git a/docs/guides/integration/pre-commit.md b/docs/guides/integration/pre-commit.md index 9e78d1595..105d6f053 100644 --- a/docs/guides/integration/pre-commit.md +++ b/docs/guides/integration/pre-commit.md @@ -19,7 +19,7 @@ To make sure your `uv.lock` file is up to date even if your `pyproject.toml` fil repos: - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.7.15 + rev: 0.7.16 hooks: - id: uv-lock ``` @@ -30,7 +30,7 @@ To keep a `requirements.txt` file in sync with your `uv.lock` file: repos: - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.7.15 + rev: 0.7.16 hooks: - id: uv-export ``` @@ -41,7 +41,7 @@ To compile requirements files: repos: - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.7.15 + rev: 0.7.16 hooks: # Compile requirements - id: pip-compile @@ -54,7 +54,7 @@ To compile alternative requirements files, modify `args` and `files`: repos: - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.7.15 + rev: 0.7.16 hooks: # Compile requirements - id: pip-compile @@ -68,7 +68,7 @@ To run the hook over multiple files at the same time, add additional entries: repos: - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.7.15 + rev: 0.7.16 hooks: # Compile requirements - id: pip-compile diff --git a/pyproject.toml b/pyproject.toml index 5ede7780c..32a928124 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "uv" -version = "0.7.15" +version = "0.7.16" description = "An extremely fast Python package and project manager, written in Rust." authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }] requires-python = ">=3.8" From 731689e503b31b3f6028d9bf8c416deadbd2c125 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 27 Jun 2025 21:39:35 -0400 Subject: [PATCH 112/185] Apply build constraints when resolving `--with` dependencies (#14340) ## Summary We were applying these at install time, but not resolve time. --- crates/uv/src/commands/project/environment.rs | 1 + crates/uv/src/commands/project/mod.rs | 2 +- crates/uv/src/commands/tool/install.rs | 2 ++ crates/uv/src/commands/tool/upgrade.rs | 1 + crates/uv/tests/it/run.rs | 5 ++--- crates/uv/tests/it/tool_install.rs | 1 - crates/uv/tests/it/tool_run.rs | 1 - 7 files changed, 7 insertions(+), 6 deletions(-) diff --git a/crates/uv/src/commands/project/environment.rs b/crates/uv/src/commands/project/environment.rs index da3dc7f63..f5a9713d2 100644 --- a/crates/uv/src/commands/project/environment.rs +++ b/crates/uv/src/commands/project/environment.rs @@ -51,6 +51,7 @@ impl CachedEnvironment { resolve_environment( spec, &interpreter, + build_constraints.clone(), &settings.resolver, network_settings, state, diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index a3249b11a..378c50aa2 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -1746,6 +1746,7 @@ impl<'lock> EnvironmentSpecification<'lock> { pub(crate) async fn resolve_environment( spec: EnvironmentSpecification<'_>, interpreter: &Interpreter, + build_constraints: Constraints, settings: &ResolverSettings, network_settings: &NetworkSettings, state: &PlatformState, @@ -1842,7 +1843,6 @@ pub(crate) async fn resolve_environment( let extras = ExtrasSpecification::default(); let groups = BTreeMap::new(); let hasher = HashStrategy::default(); - let build_constraints = Constraints::default(); let build_hasher = HashStrategy::default(); // When resolving from an interpreter, we assume an empty environment, so reinstalls and diff --git a/crates/uv/src/commands/tool/install.rs b/crates/uv/src/commands/tool/install.rs index e816e771e..5ced211b3 100644 --- a/crates/uv/src/commands/tool/install.rs +++ b/crates/uv/src/commands/tool/install.rs @@ -477,6 +477,7 @@ pub(crate) async fn install( let resolution = resolve_environment( spec.clone(), &interpreter, + Constraints::from_requirements(build_constraints.iter().cloned()), &settings.resolver, &network_settings, &state, @@ -530,6 +531,7 @@ pub(crate) async fn install( match resolve_environment( spec, &interpreter, + Constraints::from_requirements(build_constraints.iter().cloned()), &settings.resolver, &network_settings, &state, diff --git a/crates/uv/src/commands/tool/upgrade.rs b/crates/uv/src/commands/tool/upgrade.rs index 166e00349..95b7d1e2d 100644 --- a/crates/uv/src/commands/tool/upgrade.rs +++ b/crates/uv/src/commands/tool/upgrade.rs @@ -298,6 +298,7 @@ async fn upgrade_tool( let resolution = resolve_environment( spec.into(), interpreter, + build_constraints.clone(), &settings.resolver, network_settings, &state, diff --git a/crates/uv/tests/it/run.rs b/crates/uv/tests/it/run.rs index c2c9bc7a4..82f3c2b0b 100644 --- a/crates/uv/tests/it/run.rs +++ b/crates/uv/tests/it/run.rs @@ -1344,7 +1344,7 @@ fn run_with_build_constraints() -> Result<()> { })?; // Installing requests with incompatible build constraints should fail. - uv_snapshot!(context.filters(), context.run().arg("--with").arg("requests==1.2").arg("main.py"), @r###" + uv_snapshot!(context.filters(), context.run().arg("--with").arg("requests==1.2").arg("main.py"), @r" success: false exit_code: 1 ----- stdout ----- @@ -1358,12 +1358,11 @@ fn run_with_build_constraints() -> Result<()> { + idna==3.6 + sniffio==1.3.1 + typing-extensions==4.10.0 - Resolved 1 package in [TIME] × Failed to download and build `requests==1.2.0` ├─▶ Failed to resolve requirements from `setup.py` build ├─▶ No solution found when resolving: `setuptools>=40.8.0` ╰─▶ Because you require setuptools>=40.8.0 and setuptools==1, we can conclude that your requirements are unsatisfiable. - "###); + "); // Change the build constraint to be compatible with `requests==1.2`. pyproject_toml.write_str(indoc! { r#" diff --git a/crates/uv/tests/it/tool_install.rs b/crates/uv/tests/it/tool_install.rs index 88e73406f..0da627552 100644 --- a/crates/uv/tests/it/tool_install.rs +++ b/crates/uv/tests/it/tool_install.rs @@ -420,7 +420,6 @@ fn tool_install_with_incompatible_build_constraints() -> Result<()> { ----- stdout ----- ----- stderr ----- - Resolved [N] packages in [TIME] × Failed to download and build `requests==1.2.0` ├─▶ Failed to resolve requirements from `setup.py` build ├─▶ No solution found when resolving: `setuptools>=40.8.0` diff --git a/crates/uv/tests/it/tool_run.rs b/crates/uv/tests/it/tool_run.rs index a8bcd5a05..d4dcb216c 100644 --- a/crates/uv/tests/it/tool_run.rs +++ b/crates/uv/tests/it/tool_run.rs @@ -2537,7 +2537,6 @@ fn tool_run_with_incompatible_build_constraints() -> Result<()> { ----- stdout ----- ----- stderr ----- - Resolved [N] packages in [TIME] × Failed to download and build `requests==1.2.0` ├─▶ Failed to resolve requirements from `setup.py` build ├─▶ No solution found when resolving: `setuptools>=40.8.0` From db14cc3005d2cd53802cb04c2f1e177a22c934ac Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 28 Jun 2025 03:08:53 +0000 Subject: [PATCH 113/185] Sync latest Python releases (#14339) Automated update for Python releases. Co-authored-by: zanieb <2586601+zanieb@users.noreply.github.com> --- crates/uv-dev/src/generate_sysconfig_mappings.rs | 4 ++-- crates/uv-python/src/sysconfig/generated_mappings.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/uv-dev/src/generate_sysconfig_mappings.rs b/crates/uv-dev/src/generate_sysconfig_mappings.rs index 632d8f6c1..2e67f8e17 100644 --- a/crates/uv-dev/src/generate_sysconfig_mappings.rs +++ b/crates/uv-dev/src/generate_sysconfig_mappings.rs @@ -11,7 +11,7 @@ use crate::ROOT_DIR; use crate::generate_all::Mode; /// Contains current supported targets -const TARGETS_YML_URL: &str = "https://raw.githubusercontent.com/astral-sh/python-build-standalone/refs/tags/20250612/cpython-unix/targets.yml"; +const TARGETS_YML_URL: &str = "https://raw.githubusercontent.com/astral-sh/python-build-standalone/refs/tags/20250626/cpython-unix/targets.yml"; #[derive(clap::Args)] pub(crate) struct Args { @@ -130,7 +130,7 @@ async fn generate() -> Result { output.push_str("//! DO NOT EDIT\n"); output.push_str("//!\n"); output.push_str("//! Generated with `cargo run dev generate-sysconfig-metadata`\n"); - output.push_str("//! Targets from \n"); + output.push_str("//! Targets from \n"); output.push_str("//!\n"); // Disable clippy/fmt diff --git a/crates/uv-python/src/sysconfig/generated_mappings.rs b/crates/uv-python/src/sysconfig/generated_mappings.rs index 32a432b54..1ae689833 100644 --- a/crates/uv-python/src/sysconfig/generated_mappings.rs +++ b/crates/uv-python/src/sysconfig/generated_mappings.rs @@ -1,7 +1,7 @@ //! DO NOT EDIT //! //! Generated with `cargo run dev generate-sysconfig-metadata` -//! Targets from +//! Targets from //! #![allow(clippy::all)] #![cfg_attr(any(), rustfmt::skip)] From 692667cbb055c441871395eacbcc3b1eafe46dea Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Sat, 28 Jun 2025 09:42:18 -0500 Subject: [PATCH 114/185] Use the canonical `ImplementationName` -> `&str` implementation (#14337) Motivated by some code duplication highlighted in https://github.com/astral-sh/uv/pull/14201, I noticed we weren't taking advantage of the existing implementation for casting to a str here. Unfortunately, we do need a special case for CPython still. --- crates/uv-python/src/implementation.rs | 14 ++++++++++++++ crates/uv-python/src/managed.rs | 6 +----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/crates/uv-python/src/implementation.rs b/crates/uv-python/src/implementation.rs index ffc61dac7..4393d56f4 100644 --- a/crates/uv-python/src/implementation.rs +++ b/crates/uv-python/src/implementation.rs @@ -44,6 +44,13 @@ impl ImplementationName { Self::GraalPy => "GraalPy", } } + + pub fn executable_name(self) -> &'static str { + match self { + Self::CPython => "python", + Self::PyPy | Self::GraalPy => self.into(), + } + } } impl LenientImplementationName { @@ -53,6 +60,13 @@ impl LenientImplementationName { Self::Unknown(name) => name, } } + + pub fn executable_name(&self) -> &str { + match self { + Self::Known(implementation) => implementation.executable_name(), + Self::Unknown(name) => name, + } + } } impl From<&ImplementationName> for &'static str { diff --git a/crates/uv-python/src/managed.rs b/crates/uv-python/src/managed.rs index e7287fe72..ad1dacac6 100644 --- a/crates/uv-python/src/managed.rs +++ b/crates/uv-python/src/managed.rs @@ -362,11 +362,7 @@ impl ManagedPythonInstallation { /// If windowed is true, `pythonw.exe` is selected over `python.exe` on windows, with no changes /// on non-windows. pub fn executable(&self, windowed: bool) -> PathBuf { - let implementation = match self.implementation() { - ImplementationName::CPython => "python", - ImplementationName::PyPy => "pypy", - ImplementationName::GraalPy => "graalpy", - }; + let implementation = self.implementation().executable_name(); let version = match self.implementation() { ImplementationName::CPython => { From 608a1020c662936bce01cd32d43975e154812168 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Sat, 28 Jun 2025 09:42:23 -0500 Subject: [PATCH 115/185] Update the Python query cache comment (#14330) --- crates/uv-python/src/interpreter.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/uv-python/src/interpreter.rs b/crates/uv-python/src/interpreter.rs index b62633283..0cf65da10 100644 --- a/crates/uv-python/src/interpreter.rs +++ b/crates/uv-python/src/interpreter.rs @@ -977,7 +977,8 @@ impl InterpreterInfo { sys_info::os_release().unwrap_or_default(), )), // We use the absolute path for the cache entry to avoid cache collisions for relative - // paths. But we don't to query the executable with symbolic links resolved. + // paths. But we don't want to query the executable with symbolic links resolved because + // that can change reported values, e.g., `sys.executable`. format!("{}.msgpack", cache_digest(&absolute)), ); From 0cfbdcec09deb9170d062f5029a99a5979f25d6a Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Sat, 28 Jun 2025 09:42:27 -0500 Subject: [PATCH 116/185] Ignore `UV_PYTHON_CACHE_DIR` when empty (#14336) To match our semantics elsewhere --- crates/uv-python/src/downloads.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/uv-python/src/downloads.rs b/crates/uv-python/src/downloads.rs index 51f6f1d45..2b0a7e669 100644 --- a/crates/uv-python/src/downloads.rs +++ b/crates/uv-python/src/downloads.rs @@ -772,7 +772,9 @@ impl ManagedPythonDownload { let temp_dir = tempfile::tempdir_in(scratch_dir).map_err(Error::DownloadDirError)?; - if let Some(python_builds_dir) = env::var_os(EnvVars::UV_PYTHON_CACHE_DIR) { + if let Some(python_builds_dir) = + env::var_os(EnvVars::UV_PYTHON_CACHE_DIR).filter(|s| !s.is_empty()) + { let python_builds_dir = PathBuf::from(python_builds_dir); fs_err::create_dir_all(&python_builds_dir)?; let hash_prefix = match self.sha256 { From ec18f4813a31e9c9de6f20ec87ea3038bfe1ae7e Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Sat, 28 Jun 2025 11:32:03 -0500 Subject: [PATCH 117/185] Fix typo (#14341) --- docs/concepts/projects/dependencies.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/concepts/projects/dependencies.md b/docs/concepts/projects/dependencies.md index 42a579695..22d030637 100644 --- a/docs/concepts/projects/dependencies.md +++ b/docs/concepts/projects/dependencies.md @@ -37,7 +37,7 @@ dependencies = ["httpx>=0.27.2"] ``` The [`--dev`](#development-dependencies), [`--group`](#dependency-groups), or -[`--optional`](#optional-dependencies) flags can be used to add a dependencies to an alternative +[`--optional`](#optional-dependencies) flags can be used to add dependencies to an alternative field. The dependency will include a constraint, e.g., `>=0.27.2`, for the most recent, compatible version From f9d3f8ea3bf432cd2cf9c8b613be99b90568a313 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Sun, 29 Jun 2025 08:19:05 -0500 Subject: [PATCH 118/185] Fix error message ordering for `pyvenv.cfg` version conflict (#14329) These were reversed, and we're missing an "a". --- crates/uv/src/commands/project/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index 378c50aa2..c84253eaa 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -769,7 +769,7 @@ pub(crate) enum EnvironmentIncompatibilityError { RequiresPython(EnvironmentKind, RequiresPython), #[error( - "The interpreter in the {0} environment has different version ({1}) than it was created with ({2})" + "The interpreter in the {0} environment has a different version ({1}) than it was created with ({2})" )] PyenvVersionConflict(EnvironmentKind, Version, Version), } @@ -785,8 +785,8 @@ fn environment_is_usable( if let Some((cfg_version, int_version)) = environment.get_pyvenv_version_conflict() { return Err(EnvironmentIncompatibilityError::PyenvVersionConflict( kind, - cfg_version, int_version, + cfg_version, )); } From 734b228edf53984686dbb48db57aff8505695473 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 29 Jun 2025 09:36:13 -0400 Subject: [PATCH 119/185] Drop trailing slashes when converting index URL from URL (#14346) ## Summary In #14245, we started normalizing index URLs by dropping the trailing slash in the lockfile. We added tests to ensure that this didn't cause existing lockfiles to be invalidated, but we missed one of the constructors (specifically, the path that's used with `tool.uv.sources`). --- crates/uv-distribution-types/src/index_url.rs | 28 +-- crates/uv-pep508/src/verbatim_url.rs | 5 + crates/uv/tests/it/lock.rs | 183 +++++++++++++++++- 3 files changed, 201 insertions(+), 15 deletions(-) diff --git a/crates/uv-distribution-types/src/index_url.rs b/crates/uv-distribution-types/src/index_url.rs index 90b5ad809..eb91e852e 100644 --- a/crates/uv-distribution-types/src/index_url.rs +++ b/crates/uv-distribution-types/src/index_url.rs @@ -44,16 +44,9 @@ impl IndexUrl { let url = match split_scheme(path) { Some((scheme, ..)) => { match Scheme::parse(scheme) { - Some(scheme) => { - if scheme.is_file() { - // Ex) `file:///path/to/something/` - VerbatimUrl::parse_url(path)? - } else { - // Ex) `https://pypi.org/simple/` - // Remove a trailing slash if it exists. - let normalized_path = path.strip_suffix('/').unwrap_or(path); - VerbatimUrl::parse_url(normalized_path)? - } + Some(_) => { + // Ex) `https://pypi.org/simple` + VerbatimUrl::parse_url(path)? } None => { // Ex) `C:\Users\user\index` @@ -265,13 +258,20 @@ impl<'de> serde::de::Deserialize<'de> for IndexUrl { } impl From for IndexUrl { - fn from(url: VerbatimUrl) -> Self { + fn from(mut url: VerbatimUrl) -> Self { if url.scheme() == "file" { Self::Path(Arc::new(url)) - } else if *url.raw() == *PYPI_URL { - Self::Pypi(Arc::new(url)) } else { - Self::Url(Arc::new(url)) + // Remove trailing slashes for consistency. They'll be re-added if necessary when + // querying the Simple API. + if let Ok(mut path_segments) = url.raw_mut().path_segments_mut() { + path_segments.pop_if_empty(); + } + if *url.raw() == *PYPI_URL { + Self::Pypi(Arc::new(url)) + } else { + Self::Url(Arc::new(url)) + } } } } diff --git a/crates/uv-pep508/src/verbatim_url.rs b/crates/uv-pep508/src/verbatim_url.rs index 988bebc5e..8b914eb74 100644 --- a/crates/uv-pep508/src/verbatim_url.rs +++ b/crates/uv-pep508/src/verbatim_url.rs @@ -166,6 +166,11 @@ impl VerbatimUrl { &self.url } + /// Return a mutable reference to the underlying [`DisplaySafeUrl`]. + pub fn raw_mut(&mut self) -> &mut DisplaySafeUrl { + &mut self.url + } + /// Convert a [`VerbatimUrl`] into a [`DisplaySafeUrl`]. pub fn to_url(&self) -> DisplaySafeUrl { self.url.clone() diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index 5fb0fcd27..ff5465042 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -28448,6 +28448,9 @@ fn lock_with_index_trailing_slashes_in_lockfile() -> Result<()> { [[tool.uv.index]] name = "pypi-proxy" url = "https://pypi-proxy.fly.dev/simple" + + [tool.uv.sources] + anyio = { index = "pypi-proxy" } "#, )?; @@ -28491,7 +28494,185 @@ fn lock_with_index_trailing_slashes_in_lockfile() -> Result<()> { ] [package.metadata] - requires-dist = [{ name = "anyio" }] + requires-dist = [{ name = "anyio", index = "https://pypi-proxy.fly.dev/simple/" }] + + [[package]] + name = "sniffio" + version = "1.3.1" + source = { registry = "https://pypi-proxy.fly.dev/simple/" } + sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, + ] + "# + )?; + + // Run `uv lock --locked`. + uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 4 packages in [TIME] + "); + + Ok(()) +} + +/// Run `uv lock --locked` with a lockfile with trailing slashes on index URLs. +#[test] +fn lock_with_index_trailing_slashes_in_pyproject_toml() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["anyio"] + + [[tool.uv.index]] + name = "pypi-proxy" + url = "https://pypi-proxy.fly.dev/simple/" + + [tool.uv.sources] + anyio = { index = "pypi-proxy" } + "#, + )?; + + let lock = context.temp_dir.child("uv.lock"); + lock.write_str(r#" + version = 1 + revision = 2 + requires-python = ">=3.12" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [[package]] + name = "anyio" + version = "4.3.0" + source = { registry = "https://pypi-proxy.fly.dev/simple" } + dependencies = [ + { name = "idna" }, + { name = "sniffio" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6", size = 159642, upload-time = "2024-02-19T08:36:28.641Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8", size = 85584, upload-time = "2024-02-19T08:36:26.842Z" }, + ] + + [[package]] + name = "idna" + version = "3.6" + source = { registry = "https://pypi-proxy.fly.dev/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426, upload-time = "2023-11-25T15:40:54.902Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567, upload-time = "2023-11-25T15:40:52.604Z" }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "anyio" }, + ] + + [package.metadata] + requires-dist = [{ name = "anyio", index = "https://pypi-proxy.fly.dev/simple" }] + + [[package]] + name = "sniffio" + version = "1.3.1" + source = { registry = "https://pypi-proxy.fly.dev/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, + ] + "# + )?; + + // Run `uv lock --locked`. + uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 4 packages in [TIME] + "); + + Ok(()) +} + +/// Run `uv lock --locked` with a lockfile with trailing slashes on index URLs. +#[test] +fn lock_with_index_trailing_slashes_in_lockfile_and_pyproject_toml() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["anyio"] + + [[tool.uv.index]] + name = "pypi-proxy" + url = "https://pypi-proxy.fly.dev/simple/" + + [tool.uv.sources] + anyio = { index = "pypi-proxy" } + "#, + )?; + + let lock = context.temp_dir.child("uv.lock"); + lock.write_str(r#" + version = 1 + revision = 2 + requires-python = ">=3.12" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [[package]] + name = "anyio" + version = "4.3.0" + source = { registry = "https://pypi-proxy.fly.dev/simple/" } + dependencies = [ + { name = "idna" }, + { name = "sniffio" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6", size = 159642, upload-time = "2024-02-19T08:36:28.641Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8", size = 85584, upload-time = "2024-02-19T08:36:26.842Z" }, + ] + + [[package]] + name = "idna" + version = "3.6" + source = { registry = "https://pypi-proxy.fly.dev/simple/" } + sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426, upload-time = "2023-11-25T15:40:54.902Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567, upload-time = "2023-11-25T15:40:52.604Z" }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "anyio" }, + ] + + [package.metadata] + requires-dist = [{ name = "anyio", index = "https://pypi-proxy.fly.dev/simple/" }] [[package]] name = "sniffio" From 41c218a89b53a32ee51b3b069ba6407eae984ad0 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 29 Jun 2025 09:58:33 -0400 Subject: [PATCH 120/185] Bump version to 0.7.17 (#14347) --- CHANGELOG.md | 8 ++++++++ Cargo.lock | 6 +++--- crates/uv-build/Cargo.toml | 2 +- crates/uv-build/pyproject.toml | 2 +- crates/uv-version/Cargo.toml | 2 +- crates/uv/Cargo.toml | 2 +- docs/concepts/build-backend.md | 2 +- docs/getting-started/installation.md | 4 ++-- docs/guides/integration/aws-lambda.md | 4 ++-- docs/guides/integration/docker.md | 10 +++++----- docs/guides/integration/github.md | 2 +- docs/guides/integration/pre-commit.md | 10 +++++----- pyproject.toml | 2 +- 13 files changed, 32 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2d261a73..fc982ad71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ +## 0.7.17 + +### Bug fixes + +- Apply build constraints when resolving `--with` dependencies ([#14340](https://github.com/astral-sh/uv/pull/14340)) +- Drop trailing slashes when converting index URL from URL ([#14346](https://github.com/astral-sh/uv/pull/14346)) +- Ignore `UV_PYTHON_CACHE_DIR` when empty ([#14336](https://github.com/astral-sh/uv/pull/14336)) +- Fix error message ordering for `pyvenv.cfg` version conflict ([#14329](https://github.com/astral-sh/uv/pull/14329)) ## 0.7.16 diff --git a/Cargo.lock b/Cargo.lock index ef9f5b97a..0c6578998 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4570,7 +4570,7 @@ dependencies = [ [[package]] name = "uv" -version = "0.7.16" +version = "0.7.17" dependencies = [ "anstream", "anyhow", @@ -4734,7 +4734,7 @@ dependencies = [ [[package]] name = "uv-build" -version = "0.7.16" +version = "0.7.17" dependencies = [ "anyhow", "uv-build-backend", @@ -5923,7 +5923,7 @@ dependencies = [ [[package]] name = "uv-version" -version = "0.7.16" +version = "0.7.17" [[package]] name = "uv-virtualenv" diff --git a/crates/uv-build/Cargo.toml b/crates/uv-build/Cargo.toml index 6c3ab09d9..8897a421d 100644 --- a/crates/uv-build/Cargo.toml +++ b/crates/uv-build/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-build" -version = "0.7.16" +version = "0.7.17" edition.workspace = true rust-version.workspace = true homepage.workspace = true diff --git a/crates/uv-build/pyproject.toml b/crates/uv-build/pyproject.toml index 5687497ae..ae0ecde47 100644 --- a/crates/uv-build/pyproject.toml +++ b/crates/uv-build/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uv-build" -version = "0.7.16" +version = "0.7.17" description = "The uv build backend" authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }] requires-python = ">=3.8" diff --git a/crates/uv-version/Cargo.toml b/crates/uv-version/Cargo.toml index 5d3be8a9d..2d0c3d096 100644 --- a/crates/uv-version/Cargo.toml +++ b/crates/uv-version/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-version" -version = "0.7.16" +version = "0.7.17" edition = { workspace = true } rust-version = { workspace = true } homepage = { workspace = true } diff --git a/crates/uv/Cargo.toml b/crates/uv/Cargo.toml index 5f93c3ef6..5f0ea848e 100644 --- a/crates/uv/Cargo.toml +++ b/crates/uv/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv" -version = "0.7.16" +version = "0.7.17" edition = { workspace = true } rust-version = { workspace = true } homepage = { workspace = true } diff --git a/docs/concepts/build-backend.md b/docs/concepts/build-backend.md index 260680d4c..78bc84636 100644 --- a/docs/concepts/build-backend.md +++ b/docs/concepts/build-backend.md @@ -19,7 +19,7 @@ existing project, add it to the `[build-system]` section in your `pyproject.toml ```toml [build-system] -requires = ["uv_build>=0.7.16,<0.8.0"] +requires = ["uv_build>=0.7.17,<0.8.0"] build-backend = "uv_build" ``` diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index d2235fc71..62f9f50ae 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -25,7 +25,7 @@ uv provides a standalone installer to download and install uv: Request a specific version by including it in the URL: ```console - $ curl -LsSf https://astral.sh/uv/0.7.16/install.sh | sh + $ curl -LsSf https://astral.sh/uv/0.7.17/install.sh | sh ``` === "Windows" @@ -41,7 +41,7 @@ uv provides a standalone installer to download and install uv: Request a specific version by including it in the URL: ```pwsh-session - PS> powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/0.7.16/install.ps1 | iex" + PS> powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/0.7.17/install.ps1 | iex" ``` !!! tip diff --git a/docs/guides/integration/aws-lambda.md b/docs/guides/integration/aws-lambda.md index 25be2ae38..a0ee99671 100644 --- a/docs/guides/integration/aws-lambda.md +++ b/docs/guides/integration/aws-lambda.md @@ -92,7 +92,7 @@ the second stage, we'll copy this directory over to the final image, omitting th other unnecessary files. ```dockerfile title="Dockerfile" -FROM ghcr.io/astral-sh/uv:0.7.16 AS uv +FROM ghcr.io/astral-sh/uv:0.7.17 AS uv # First, bundle the dependencies into the task root. FROM public.ecr.aws/lambda/python:3.13 AS builder @@ -334,7 +334,7 @@ And confirm that opening http://127.0.0.1:8000/ in a web browser displays, "Hell Finally, we'll update the Dockerfile to include the local library in the deployment package: ```dockerfile title="Dockerfile" -FROM ghcr.io/astral-sh/uv:0.7.16 AS uv +FROM ghcr.io/astral-sh/uv:0.7.17 AS uv # First, bundle the dependencies into the task root. FROM public.ecr.aws/lambda/python:3.13 AS builder diff --git a/docs/guides/integration/docker.md b/docs/guides/integration/docker.md index 72d730362..48d7b6399 100644 --- a/docs/guides/integration/docker.md +++ b/docs/guides/integration/docker.md @@ -31,7 +31,7 @@ $ docker run --rm -it ghcr.io/astral-sh/uv:debian uv --help The following distroless images are available: - `ghcr.io/astral-sh/uv:latest` -- `ghcr.io/astral-sh/uv:{major}.{minor}.{patch}`, e.g., `ghcr.io/astral-sh/uv:0.7.16` +- `ghcr.io/astral-sh/uv:{major}.{minor}.{patch}`, e.g., `ghcr.io/astral-sh/uv:0.7.17` - `ghcr.io/astral-sh/uv:{major}.{minor}`, e.g., `ghcr.io/astral-sh/uv:0.7` (the latest patch version) @@ -75,7 +75,7 @@ And the following derived images are available: As with the distroless image, each derived image is published with uv version tags as `ghcr.io/astral-sh/uv:{major}.{minor}.{patch}-{base}` and -`ghcr.io/astral-sh/uv:{major}.{minor}-{base}`, e.g., `ghcr.io/astral-sh/uv:0.7.16-alpine`. +`ghcr.io/astral-sh/uv:{major}.{minor}-{base}`, e.g., `ghcr.io/astral-sh/uv:0.7.17-alpine`. For more details, see the [GitHub Container](https://github.com/astral-sh/uv/pkgs/container/uv) page. @@ -113,7 +113,7 @@ Note this requires `curl` to be available. In either case, it is best practice to pin to a specific uv version, e.g., with: ```dockerfile -COPY --from=ghcr.io/astral-sh/uv:0.7.16 /uv /uvx /bin/ +COPY --from=ghcr.io/astral-sh/uv:0.7.17 /uv /uvx /bin/ ``` !!! tip @@ -131,7 +131,7 @@ COPY --from=ghcr.io/astral-sh/uv:0.7.16 /uv /uvx /bin/ Or, with the installer: ```dockerfile -ADD https://astral.sh/uv/0.7.16/install.sh /uv-installer.sh +ADD https://astral.sh/uv/0.7.17/install.sh /uv-installer.sh ``` ### Installing a project @@ -557,5 +557,5 @@ Verified OK !!! tip These examples use `latest`, but best practice is to verify the attestation for a specific - version tag, e.g., `ghcr.io/astral-sh/uv:0.7.16`, or (even better) the specific image digest, + version tag, e.g., `ghcr.io/astral-sh/uv:0.7.17`, or (even better) the specific image digest, such as `ghcr.io/astral-sh/uv:0.5.27@sha256:5adf09a5a526f380237408032a9308000d14d5947eafa687ad6c6a2476787b4f`. diff --git a/docs/guides/integration/github.md b/docs/guides/integration/github.md index 56fc51e21..f58fe2782 100644 --- a/docs/guides/integration/github.md +++ b/docs/guides/integration/github.md @@ -47,7 +47,7 @@ jobs: uses: astral-sh/setup-uv@v5 with: # Install a specific version of uv. - version: "0.7.16" + version: "0.7.17" ``` ## Setting up Python diff --git a/docs/guides/integration/pre-commit.md b/docs/guides/integration/pre-commit.md index 105d6f053..71fe7394f 100644 --- a/docs/guides/integration/pre-commit.md +++ b/docs/guides/integration/pre-commit.md @@ -19,7 +19,7 @@ To make sure your `uv.lock` file is up to date even if your `pyproject.toml` fil repos: - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.7.16 + rev: 0.7.17 hooks: - id: uv-lock ``` @@ -30,7 +30,7 @@ To keep a `requirements.txt` file in sync with your `uv.lock` file: repos: - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.7.16 + rev: 0.7.17 hooks: - id: uv-export ``` @@ -41,7 +41,7 @@ To compile requirements files: repos: - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.7.16 + rev: 0.7.17 hooks: # Compile requirements - id: pip-compile @@ -54,7 +54,7 @@ To compile alternative requirements files, modify `args` and `files`: repos: - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.7.16 + rev: 0.7.17 hooks: # Compile requirements - id: pip-compile @@ -68,7 +68,7 @@ To run the hook over multiple files at the same time, add additional entries: repos: - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.7.16 + rev: 0.7.17 hooks: # Compile requirements - id: pip-compile diff --git a/pyproject.toml b/pyproject.toml index 32a928124..078bd4ebd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "uv" -version = "0.7.16" +version = "0.7.17" description = "An extremely fast Python package and project manager, written in Rust." authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }] requires-python = ">=3.8" From c0ebe6871d46b9cb1f4a2dcaf2e409beeaf7a4eb Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Sun, 29 Jun 2025 09:40:29 -0500 Subject: [PATCH 121/185] Improve trace message for cached Python interpreter query (#14328) --- crates/uv-python/src/interpreter.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/uv-python/src/interpreter.rs b/crates/uv-python/src/interpreter.rs index 0cf65da10..df27b497b 100644 --- a/crates/uv-python/src/interpreter.rs +++ b/crates/uv-python/src/interpreter.rs @@ -1016,7 +1016,7 @@ impl InterpreterInfo { Ok(cached) => { if cached.timestamp == modified { trace!( - "Cached interpreter info for Python {}, skipping probing: {}", + "Found cached interpreter info for Python {}, skipping query of: {}", cached.data.markers.python_full_version(), executable.user_display() ); From 17b7eec287572214676f926ef1ee5d4fc502abf3 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Sun, 29 Jun 2025 12:25:10 -0500 Subject: [PATCH 122/185] Consistently normalize trailing slashes on URLs with no path segments (#14349) Alternative to https://github.com/astral-sh/uv/pull/14348 --- crates/uv-distribution-types/src/file.rs | 26 ++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/crates/uv-distribution-types/src/file.rs b/crates/uv-distribution-types/src/file.rs index 948378c0c..81e3e2878 100644 --- a/crates/uv-distribution-types/src/file.rs +++ b/crates/uv-distribution-types/src/file.rs @@ -171,10 +171,21 @@ impl UrlString { } /// Return the [`UrlString`] (as a [`Cow`]) with trailing slash removed. + /// + /// This matches the semantics of [`Url::pop_if_empty`], which will not trim a trailing slash if + /// it's the only path segment, e.g., `https://example.com/` would be unchanged. #[must_use] pub fn without_trailing_slash(&self) -> Cow<'_, Self> { self.as_ref() .strip_suffix('/') + .filter(|path| { + // Only strip the trailing slash if there's _another_ trailing slash that isn't a + // part of the scheme. + path.split_once("://") + .map(|(_scheme, rest)| rest) + .unwrap_or(path) + .contains('/') + }) .map(|path| Cow::Owned(UrlString(SmallString::from(path)))) .unwrap_or(Cow::Borrowed(self)) } @@ -283,5 +294,20 @@ mod tests { url.without_trailing_slash(), Cow::Owned(UrlString("https://example.com/path".into())) ); + + // Does not remove a trailing slash if it's the only path segment + let url = UrlString("https://example.com/".into()); + assert_eq!(url.without_trailing_slash(), Cow::Borrowed(&url)); + + // Does not remove a trailing slash if it's the only path segment with a missing scheme + let url = UrlString("example.com/".into()); + assert_eq!(url.without_trailing_slash(), Cow::Borrowed(&url)); + + // Removes the trailing slash when the scheme is missing + let url = UrlString("example.com/path/".into()); + assert_eq!( + url.without_trailing_slash(), + Cow::Owned(UrlString("example.com/path".into())) + ); } } From d15efb7d918960473eed4ca4b12fd07afc8b77dd Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 29 Jun 2025 15:07:07 -0400 Subject: [PATCH 123/185] Add an `IntoIterator` for `FormMetadata` (#14351) ## Summary Clippy would lint for this if the symbol were public as a matter of API hygiene, so adding it. --- crates/uv-publish/src/lib.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/uv-publish/src/lib.rs b/crates/uv-publish/src/lib.rs index 51f9bb472..f3dc768c6 100644 --- a/crates/uv-publish/src/lib.rs +++ b/crates/uv-publish/src/lib.rs @@ -758,6 +758,14 @@ impl FormMetadata { } } +impl<'a> IntoIterator for &'a FormMetadata { + type Item = &'a (&'a str, String); + type IntoIter = std::slice::Iter<'a, (&'a str, String)>; + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + /// Build the upload request. /// /// Returns the request and the reporter progress bar id. From 7603153f5b1ecfdd81e921b3915bb7b75d817ef7 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 29 Jun 2025 15:30:52 -0400 Subject: [PATCH 124/185] Allow `alpha`, `beta`, and `rc` prefixes in tests (#14352) ## Summary A bunch of tests currently fail if you try to use a pre-release version. This PR makes the regular expressions more lenient. --- crates/uv/tests/it/common/mod.rs | 4 ++-- crates/uv/tests/it/version.rs | 14 ++++++++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/crates/uv/tests/it/common/mod.rs b/crates/uv/tests/it/common/mod.rs index 9ead0a7c2..4c411899c 100644 --- a/crates/uv/tests/it/common/mod.rs +++ b/crates/uv/tests/it/common/mod.rs @@ -66,7 +66,7 @@ pub const INSTA_FILTERS: &[(&str, &str)] = &[ (r"uv\.exe", "uv"), // uv version display ( - r"uv(-.*)? \d+\.\d+\.\d+(\+\d+)?( \(.*\))?", + r"uv(-.*)? \d+\.\d+\.\d+(-(alpha|beta|rc)\.\d+)?(\+\d+)?( \([^)]*\))?", r"uv [VERSION] ([COMMIT] DATE)", ), // Trim end-of-line whitespaces, to allow removing them on save. @@ -254,7 +254,7 @@ impl TestContext { let added_filters = [ (r"home = .+".to_string(), "home = [PYTHON_HOME]".to_string()), ( - r"uv = \d+\.\d+\.\d+".to_string(), + r"uv = \d+\.\d+\.\d+(-(alpha|beta|rc)\.\d+)?(\+\d+)?".to_string(), "uv = [UV_VERSION]".to_string(), ), ( diff --git a/crates/uv/tests/it/version.rs b/crates/uv/tests/it/version.rs index 1fda42705..152cedaec 100644 --- a/crates/uv/tests/it/version.rs +++ b/crates/uv/tests/it/version.rs @@ -905,7 +905,7 @@ fn version_get_fallback_unmanaged_short() -> Result<()> { .filters() .into_iter() .chain([( - r"\d+\.\d+\.\d+(\+\d+)?( \(.*\))?", + r"\d+\.\d+\.\d+(-alpha\.\d+)?(\+\d+)?( \(.*\))?", r"[VERSION] ([COMMIT] DATE)", )]) .collect::>(); @@ -972,7 +972,10 @@ fn version_get_fallback_unmanaged_json() -> Result<()> { .filters() .into_iter() .chain([ - (r#"version": "\d+.\d+.\d+""#, r#"version": "[VERSION]""#), + ( + r#"version": "\d+\.\d+\.\d+(-(alpha|beta|rc)\.\d+)?(\+\d+)?""#, + r#"version": "[VERSION]""#, + ), ( r#"short_commit_hash": ".*""#, r#"short_commit_hash": "[HASH]""#, @@ -1175,7 +1178,7 @@ fn self_version_short() -> Result<()> { .filters() .into_iter() .chain([( - r"\d+\.\d+\.\d+(\+\d+)?( \(.*\))?", + r"\d+\.\d+\.\d+(-alpha\.\d+)?(\+\d+)?( \(.*\))?", r"[VERSION] ([COMMIT] DATE)", )]) .collect::>(); @@ -1220,7 +1223,10 @@ fn self_version_json() -> Result<()> { .filters() .into_iter() .chain([ - (r#"version": "\d+.\d+.\d+""#, r#"version": "[VERSION]""#), + ( + r#"version": "\d+\.\d+\.\d+(-(alpha|beta|rc)\.\d+)?(\+\d+)?""#, + r#"version": "[VERSION]""#, + ), ( r#"short_commit_hash": ".*""#, r#"short_commit_hash": "[HASH]""#, From d7e1fced434c85fb040f58b5eae74bdf61f78607 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 29 Jun 2025 22:17:41 -0400 Subject: [PATCH 125/185] Update Rust crate cargo-util to v0.2.21 (#14356) --- Cargo.lock | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0c6578998..87429b1a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -512,9 +512,9 @@ dependencies = [ [[package]] name = "cargo-util" -version = "0.2.20" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767bc85f367f6483a6072430b56f5c0d6ee7636751a21a800526d0711753d76" +checksum = "c95ec8b2485b20aed818bd7460f8eecc6c87c35c84191b353a3aba9aa1736c36" dependencies = [ "anyhow", "core-foundation", @@ -738,7 +738,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" dependencies = [ "lazy_static", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -1115,7 +1115,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -1930,7 +1930,7 @@ checksum = "e19b23d53f35ce9f56aebc7d1bb4e6ac1e9c0db7ac85c8d1760c04379edced37" dependencies = [ "hermit-abi 0.4.0", "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -1990,7 +1990,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -2849,7 +2849,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -3286,7 +3286,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -3299,7 +3299,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.9.2", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -3881,7 +3881,7 @@ dependencies = [ "getrandom 0.3.1", "once_cell", "rustix 1.0.7", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -6244,7 +6244,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] From a8b838dee903c97cb15de55e19d691ff0b0f6ded Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 29 Jun 2025 22:17:48 -0400 Subject: [PATCH 126/185] Update astral-sh/setup-uv action to v6.3.1 (#14360) --- .github/workflows/ci.yml | 10 +++++----- .github/workflows/publish-pypi.yml | 4 ++-- .github/workflows/sync-python-releases.yml | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0ef7244d5..014c12c3a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -82,7 +82,7 @@ jobs: run: rustup component add rustfmt - name: "Install uv" - uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0 + uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1 - name: "rustfmt" run: cargo fmt --all --check @@ -213,7 +213,7 @@ jobs: - name: "Install Rust toolchain" run: rustup show - - uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0 + - uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1 - name: "Install required Python versions" run: uv python install @@ -245,7 +245,7 @@ jobs: - name: "Install Rust toolchain" run: rustup show - - uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0 + - uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1 - name: "Install required Python versions" run: uv python install @@ -279,7 +279,7 @@ jobs: run: | Copy-Item -Path "${{ github.workspace }}" -Destination "${{ env.UV_WORKSPACE }}" -Recurse - - uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0 + - uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1 - name: "Install required Python versions" run: uv python install @@ -430,7 +430,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 - - uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0 + - uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1 - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 - name: "Add SSH key" if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }} diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 84c8e44d9..e4435ff17 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -22,7 +22,7 @@ jobs: id-token: write steps: - name: "Install uv" - uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0 + uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1 - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: pattern: wheels_uv-* @@ -43,7 +43,7 @@ jobs: id-token: write steps: - name: "Install uv" - uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0 + uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1 - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: pattern: wheels_uv_build-* diff --git a/.github/workflows/sync-python-releases.yml b/.github/workflows/sync-python-releases.yml index 14b572e08..166458507 100644 --- a/.github/workflows/sync-python-releases.yml +++ b/.github/workflows/sync-python-releases.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0 + - uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1 with: version: "latest" enable-cache: true From 40386e438ff7ed8c1d0792981f24882a5b074a8b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 29 Jun 2025 22:17:59 -0400 Subject: [PATCH 127/185] Update Rust crate owo-colors to v4.2.2 (#14357) --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 87429b1a7..21d767adb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2474,9 +2474,9 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "owo-colors" -version = "4.2.1" +version = "4.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26995317201fa17f3656c36716aed4a7c81743a9634ac4c99c0eeda495db0cec" +checksum = "48dd4f4a2c8405440fd0462561f0e5806bd0f77e86f51c761481bdd4018b545e" [[package]] name = "parking" From e9533a0e296255162736c165927ba289f3fcaaf8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 29 Jun 2025 22:18:12 -0400 Subject: [PATCH 128/185] Update aws-actions/configure-aws-credentials digest to 3d8cba3 (#14354) --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 014c12c3a..7ca7d9b6b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1443,7 +1443,7 @@ jobs: run: chmod +x ./uv - name: "Configure AWS credentials" - uses: aws-actions/configure-aws-credentials@3bb878b6ab43ba8717918141cd07a0ea68cfe7ea + uses: aws-actions/configure-aws-credentials@3d8cba388a057b13744d61818a337e40a119b1a7 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} From e44a64ee133fe4c3c4fecb1c7a4c60043f965470 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 29 Jun 2025 22:18:26 -0400 Subject: [PATCH 129/185] Update Rust crate windows-registry to v0.5.3 (#14359) --- Cargo.lock | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 21d767adb..416bb3606 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5602,7 +5602,7 @@ dependencies = [ "uv-trampoline-builder", "uv-warnings", "which", - "windows-registry 0.5.2", + "windows-registry 0.5.3", "windows-result 0.3.4", "windows-sys 0.59.0", ] @@ -5806,7 +5806,7 @@ dependencies = [ "tracing", "uv-fs", "uv-static", - "windows-registry 0.5.2", + "windows-registry 0.5.3", "windows-result 0.3.4", "windows-sys 0.59.0", ] @@ -6330,7 +6330,7 @@ dependencies = [ "windows-interface 0.59.1", "windows-link", "windows-result 0.3.4", - "windows-strings 0.4.1", + "windows-strings 0.4.2", ] [[package]] @@ -6400,9 +6400,9 @@ dependencies = [ [[package]] name = "windows-link" -version = "0.1.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" [[package]] name = "windows-numerics" @@ -6427,13 +6427,13 @@ dependencies = [ [[package]] name = "windows-registry" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3bab093bdd303a1240bb99b8aba8ea8a69ee19d34c9e2ef9594e708a4878820" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" dependencies = [ "windows-link", "windows-result 0.3.4", - "windows-strings 0.4.1", + "windows-strings 0.4.2", ] [[package]] @@ -6465,9 +6465,9 @@ dependencies = [ [[package]] name = "windows-strings" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a7ab927b2637c19b3dbe0965e75d8f2d30bdd697a1516191cad2ec4df8fb28a" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ "windows-link", ] From b2979d25a81423c69e31ad59892a3b584a99191d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 29 Jun 2025 22:19:38 -0400 Subject: [PATCH 130/185] Update acj/freebsd-firecracker-action action to v0.5.1 (#14355) --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7ca7d9b6b..797ccd303 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -661,7 +661,7 @@ jobs: cross build --target x86_64-unknown-freebsd - name: Test in Firecracker VM - uses: acj/freebsd-firecracker-action@6c57bda7113c2f137ef00d54512d61ae9d64365b # v0.5.0 + uses: acj/freebsd-firecracker-action@136ca0bce2adade21e526ceb07db643ad23dd2dd # v0.5.1 with: verbose: false checkout: false From 61482da31924407ca004b73610600bf9137c3e02 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 30 Jun 2025 12:13:33 +0200 Subject: [PATCH 131/185] Update pre-commit hook astral-sh/ruff-pre-commit to v0.12.1 (#14362) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [astral-sh/ruff-pre-commit](https://redirect.github.com/astral-sh/ruff-pre-commit) | repository | minor | `v0.11.13` -> `v0.12.1` | --- > [!WARNING] > Some dependencies could not be looked up. Check the Dependency Dashboard for more information. Note: The `pre-commit` manager in Renovate is not supported by the `pre-commit` maintainers or community. Please do not report any problems there, instead [create a Discussion in the Renovate repository](https://redirect.github.com/renovatebot/renovate/discussions/new) if you have any questions. --- ### Release Notes
    astral-sh/ruff-pre-commit (astral-sh/ruff-pre-commit) ### [`v0.12.1`](https://redirect.github.com/astral-sh/ruff-pre-commit/releases/tag/v0.12.1) [Compare Source](https://redirect.github.com/astral-sh/ruff-pre-commit/compare/v0.12.0...v0.12.1) See: https://github.com/astral-sh/ruff/releases/tag/0.12.1 ### [`v0.12.0`](https://redirect.github.com/astral-sh/ruff-pre-commit/releases/tag/v0.12.0) [Compare Source](https://redirect.github.com/astral-sh/ruff-pre-commit/compare/v0.11.13...v0.12.0) See: https://github.com/astral-sh/ruff/releases/tag/0.12.0
    --- ### Configuration 📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/astral-sh/uv). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 982d8f296..2280f337b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -42,7 +42,7 @@ repos: types_or: [yaml, json5] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.13 + rev: v0.12.1 hooks: - id: ruff-format - id: ruff From 15551a0201c616c005f605c973772c38b423e7d4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 30 Jun 2025 12:24:54 +0200 Subject: [PATCH 132/185] Update Swatinem/rust-cache action to v2.8.0 (#14366) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [Swatinem/rust-cache](https://redirect.github.com/Swatinem/rust-cache) | action | minor | `v2.7.8` -> `v2.8.0` | --- > [!WARNING] > Some dependencies could not be looked up. Check the Dependency Dashboard for more information. --- ### Release Notes
    Swatinem/rust-cache (Swatinem/rust-cache) ### [`v2.8.0`](https://redirect.github.com/Swatinem/rust-cache/releases/tag/v2.8.0) [Compare Source](https://redirect.github.com/Swatinem/rust-cache/compare/v2.7.8...v2.8.0) ##### What's Changed - Add cache-workspace-crates feature by [@​jbransen](https://redirect.github.com/jbransen) in [https://github.com/Swatinem/rust-cache/pull/246](https://redirect.github.com/Swatinem/rust-cache/pull/246) - Feat: support warpbuild cache provider by [@​stegaBOB](https://redirect.github.com/stegaBOB) in [https://github.com/Swatinem/rust-cache/pull/247](https://redirect.github.com/Swatinem/rust-cache/pull/247) ##### New Contributors - [@​jbransen](https://redirect.github.com/jbransen) made their first contribution in [https://github.com/Swatinem/rust-cache/pull/246](https://redirect.github.com/Swatinem/rust-cache/pull/246) - [@​stegaBOB](https://redirect.github.com/stegaBOB) made their first contribution in [https://github.com/Swatinem/rust-cache/pull/247](https://redirect.github.com/Swatinem/rust-cache/pull/247) **Full Changelog**: https://github.com/Swatinem/rust-cache/compare/v2.7.8...v2.8.0
    --- ### Configuration 📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/astral-sh/uv). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 797ccd303..870171cdc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -126,7 +126,7 @@ jobs: name: "cargo clippy | ubuntu" steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 + - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0 with: save-if: ${{ github.ref == 'refs/heads/main' }} - name: "Check uv_build dependencies" @@ -156,7 +156,7 @@ jobs: run: | Copy-Item -Path "${{ github.workspace }}" -Destination "${{ env.UV_WORKSPACE }}" -Recurse - - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 + - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0 with: workspaces: ${{ env.UV_WORKSPACE }} @@ -175,7 +175,7 @@ jobs: name: "cargo dev generate-all" steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 + - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0 with: save-if: ${{ github.ref == 'refs/heads/main' }} - name: "Generate all" @@ -208,7 +208,7 @@ jobs: - uses: rui314/setup-mold@v1 - - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 + - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0 - name: "Install Rust toolchain" run: rustup show @@ -240,7 +240,7 @@ jobs: - uses: rui314/setup-mold@v1 - - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 + - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0 - name: "Install Rust toolchain" run: rustup show @@ -283,7 +283,7 @@ jobs: - name: "Install required Python versions" run: uv python install - - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 + - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0 with: workspaces: ${{ env.UV_WORKSPACE }} @@ -332,7 +332,7 @@ jobs: run: | Copy-Item -Path "${{ github.workspace }}" -Destination "${{ env.UV_WORKSPACE }}" -Recurse - - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 + - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0 with: workspaces: ${{ env.UV_WORKSPACE }}/crates/uv-trampoline @@ -388,7 +388,7 @@ jobs: - name: Copy Git Repo to Dev Drive run: | Copy-Item -Path "${{ github.workspace }}" -Destination "${{ env.UV_WORKSPACE }}" -Recurse - - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 + - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0 with: workspaces: ${{ env.UV_WORKSPACE }}/crates/uv-trampoline - name: "Install Rust toolchain" @@ -456,7 +456,7 @@ jobs: - uses: rui314/setup-mold@v1 - - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 + - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0 - name: "Build" run: cargo build @@ -486,7 +486,7 @@ jobs: sudo apt-get install musl-tools rustup target add x86_64-unknown-linux-musl - - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 + - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0 - name: "Build" run: cargo build --target x86_64-unknown-linux-musl --bin uv --bin uvx @@ -511,7 +511,7 @@ jobs: - uses: rui314/setup-mold@v1 - - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 + - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0 - name: "Build" run: cargo build --bin uv --bin uvx @@ -535,7 +535,7 @@ jobs: - uses: rui314/setup-mold@v1 - - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 + - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0 - name: "Build" run: cargo build --bin uv --bin uvx @@ -565,7 +565,7 @@ jobs: run: | Copy-Item -Path "${{ github.workspace }}" -Destination "${{ env.UV_WORKSPACE }}" -Recurse - - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 + - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0 with: workspaces: ${{ env.UV_WORKSPACE }} @@ -600,7 +600,7 @@ jobs: run: | Copy-Item -Path "${{ github.workspace }}" -Destination "${{ env.UV_WORKSPACE }}" -Recurse - - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 + - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0 with: workspaces: ${{ env.UV_WORKSPACE }} @@ -637,7 +637,7 @@ jobs: run: rustup default ${{ steps.msrv.outputs.value }} - name: "Install mold" uses: rui314/setup-mold@v1 - - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 + - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0 - run: cargo +${{ steps.msrv.outputs.value }} build - run: ./target/debug/uv --version @@ -650,7 +650,7 @@ jobs: steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 + - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0 - name: "Cross build" run: | # Install cross from `freebsd-firecracker` @@ -2337,7 +2337,7 @@ jobs: - name: "Checkout Branch" uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 + - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0 - name: "Install Rust toolchain" run: rustup show @@ -2374,7 +2374,7 @@ jobs: - name: "Checkout Branch" uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 + - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0 - name: "Install Rust toolchain" run: rustup show From 5cfabd7085bd265fcc74bd227edf521a53a0aac3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 30 Jun 2025 13:39:20 +0000 Subject: [PATCH 133/185] Update Rust crate schemars to v1.0.3 (#14358) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [schemars](https://graham.cool/schemars/) ([source](https://redirect.github.com/GREsau/schemars)) | workspace.dependencies | patch | `1.0.0` -> `1.0.3` | --- > [!WARNING] > Some dependencies could not be looked up. Check the Dependency Dashboard for more information. --- ### Release Notes
    GREsau/schemars (schemars) ### [`v1.0.3`](https://redirect.github.com/GREsau/schemars/blob/HEAD/CHANGELOG.md#103---2025-06-28) [Compare Source](https://redirect.github.com/GREsau/schemars/compare/v1.0.2...v1.0.3) ##### Fixed - Fix compile error when a doc comment is set on both a `transparent` (or newtype) struct and its field ([https://github.com/GREsau/schemars/issues/446](https://redirect.github.com/GREsau/schemars/issues/446)) - Fix `json_schema!()` macro compatibility when used from pre-2021 rust editions ([https://github.com/GREsau/schemars/pull/447](https://redirect.github.com/GREsau/schemars/pull/447)) ### [`v1.0.2`](https://redirect.github.com/GREsau/schemars/blob/HEAD/CHANGELOG.md#102---2025-06-26) [Compare Source](https://redirect.github.com/GREsau/schemars/compare/v1.0.1...v1.0.2) ##### Fixed - Fix schema properties being incorrectly reordered during serialization ([https://github.com/GREsau/schemars/issues/444](https://redirect.github.com/GREsau/schemars/issues/444)) ### [`v1.0.1`](https://redirect.github.com/GREsau/schemars/blob/HEAD/CHANGELOG.md#101---2025-06-24) [Compare Source](https://redirect.github.com/GREsau/schemars/compare/v1.0.0...v1.0.1) ##### Fixed - Deriving `JsonSchema` with `no_std` broken due to `std::borrow::ToOwned` trait not being in scope ([https://github.com/GREsau/schemars/issues/441](https://redirect.github.com/GREsau/schemars/issues/441))
    --- ### Configuration 📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/astral-sh/uv). --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: konstin --- Cargo.lock | 8 ++++---- uv.schema.json | 18 +++++++++--------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 416bb3606..eb2faac26 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3405,9 +3405,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.0.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "febc07c7e70b5db4f023485653c754d76e1bbe8d9dbfa20193ce13da9f9633f4" +checksum = "1375ba8ef45a6f15d83fa8748f1079428295d403d6ea991d09ab100155fbc06d" dependencies = [ "dyn-clone", "ref-cast", @@ -3419,9 +3419,9 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "1.0.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1eeedaab7b1e1d09b5b4661121f4d27f9e7487089b0117833ccd7a882ee1ecc" +checksum = "2b13ed22d6d49fe23712e068770b5c4df4a693a2b02eeff8e7ca3135627a24f6" dependencies = [ "proc-macro2", "quote", diff --git a/uv.schema.json b/uv.schema.json index b00463ee9..26d6ac7a3 100644 --- a/uv.schema.json +++ b/uv.schema.json @@ -894,15 +894,6 @@ "Index": { "type": "object", "properties": { - "format": { - "description": "The format used by the index.\n\nIndexes can either be PEP 503-compliant (i.e., a PyPI-style registry implementing the Simple\nAPI) or structured as a flat list of distributions (e.g., `--find-links`). In both cases,\nindexes can point to either local or remote resources.", - "allOf": [ - { - "$ref": "#/definitions/IndexFormat" - } - ], - "default": "simple" - }, "authenticate": { "description": "When uv should use authentication for requests to the index.\n\n```toml\n[[tool.uv.index]]\nname = \"my-index\"\nurl = \"https:///simple\"\nauthenticate = \"always\"\n```", "allOf": [ @@ -922,6 +913,15 @@ "type": "boolean", "default": false }, + "format": { + "description": "The format used by the index.\n\nIndexes can either be PEP 503-compliant (i.e., a PyPI-style registry implementing the Simple\nAPI) or structured as a flat list of distributions (e.g., `--find-links`). In both cases,\nindexes can point to either local or remote resources.", + "allOf": [ + { + "$ref": "#/definitions/IndexFormat" + } + ], + "default": "simple" + }, "ignore-error-codes": { "description": "Status codes that uv should ignore when deciding whether\nto continue searching in the next index after a failure.\n\n```toml\n[[tool.uv.index]]\nname = \"my-index\"\nurl = \"https:///simple\"\nignore-error-codes = [401, 403]\n```", "type": [ From ae500c95d20df9b372be4c946cc143115ba79755 Mon Sep 17 00:00:00 2001 From: Ondrej Profant Date: Mon, 30 Jun 2025 15:58:55 +0200 Subject: [PATCH 134/185] Docs: add instructions for publishing to JFrog's Artifactory (#14253) ## Summary Add instructions for publishing to JFrog's Artifactory into [documentation](https://docs.astral.sh/uv/guides/integration/alternative-indexes/). Related issues: https://github.com/astral-sh/uv/issues/9845 https://github.com/astral-sh/uv/issues/10193 ## Test Plan I ran the documentation locally and use npx prettier. --------- Co-authored-by: Ondrej Profant Co-authored-by: Zanie Blue --- .../guides/integration/alternative-indexes.md | 66 ++++++++++++++++++- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/docs/guides/integration/alternative-indexes.md b/docs/guides/integration/alternative-indexes.md index 0258e4a74..3e73efff0 100644 --- a/docs/guides/integration/alternative-indexes.md +++ b/docs/guides/integration/alternative-indexes.md @@ -368,6 +368,68 @@ $ uv publish Note this method is not preferable because uv cannot check if the package is already published before uploading artifacts. -## Other package indexes +## JFrog Artifactory -uv is also known to work with JFrog's Artifactory. +uv can install packages from JFrog Artifactory, either by using a username and password or a JWT +token. + +To use it, add the index to your project: + +```toml title="pyproject.toml" +[[tool.uv.index]] +name = "private-registry" +url = "https://.jfrog.io/artifactory/api/pypi//simple" +``` + +### Authenticate with username and password + +```console +$ export UV_INDEX_PRIVATE_REGISTRY_USERNAME="" +$ export UV_INDEX_PRIVATE_REGISTRY_PASSWORD="" +``` + +### Authenticate with JWT token + +```console +$ export UV_INDEX_PRIVATE_REGISTRY_USERNAME="" +$ export UV_INDEX_PRIVATE_REGISTRY_PASSWORD="$JFROG_JWT_TOKEN" +``` + +!!! note + + Replace `PRIVATE_REGISTRY` in the environment variable names with the actual index name defined in your `pyproject.toml`. + +### Publishing packages to JFrog Artifactory + +Add a `publish-url` to your index definition: + +```toml title="pyproject.toml" +[[tool.uv.index]] +name = "private-registry" +url = "https://.jfrog.io/artifactory/api/pypi//simple" +publish-url = "https://.jfrog.io/artifactory/api/pypi/" +``` + +!!! important + + If you use `--token "$JFROG_TOKEN"` or `UV_PUBLISH_TOKEN` with JFrog, you will receive a + 401 Unauthorized error as JFrog requires an empty username but uv passes `__token__` for as + the username when `--token` is used. + +To authenticate, pass your token as the password and set the username to an empty string: + +```console +$ uv publish --index -u "" -p "$JFROG_TOKEN" +``` + +Alternatively, you can set environment variables: + +```console +$ export UV_PUBLISH_USERNAME="" +$ export UV_PUBLISH_PASSWORD="$JFROG_TOKEN" +$ uv publish --index private-registry +``` + +!!! note + + The publish environment variables (`UV_PUBLISH_USERNAME` and `UV_PUBLISH_PASSWORD`) do not include the index name. From 0372a5b05d21052a337fcbd64957223f869fbed2 Mon Sep 17 00:00:00 2001 From: konsti Date: Mon, 30 Jun 2025 16:32:28 +0200 Subject: [PATCH 135/185] Ignore invalid build backend settings when not building (#14372) Fixes #14323 --- crates/uv-workspace/src/pyproject.rs | 44 +++++++++++++++++++++++++++- crates/uv/tests/it/build_backend.rs | 37 +++++++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/crates/uv-workspace/src/pyproject.rs b/crates/uv-workspace/src/pyproject.rs index dabbe33fb..41e20914f 100644 --- a/crates/uv-workspace/src/pyproject.rs +++ b/crates/uv-workspace/src/pyproject.rs @@ -24,6 +24,7 @@ use uv_fs::{PortablePathBuf, relative_to}; use uv_git_types::GitReference; use uv_macros::OptionsMetadata; use uv_normalize::{DefaultGroups, ExtraName, GroupName, PackageName}; +use uv_options_metadata::{OptionSet, OptionsMetadata, Visit}; use uv_pep440::{Version, VersionSpecifiers}; use uv_pep508::MarkerTree; use uv_pypi_types::{ @@ -610,7 +611,7 @@ pub struct ToolUv { /// Note that those settings only apply when using the `uv_build` backend, other build backends /// (such as hatchling) have their own configuration. #[option_group] - pub build_backend: Option, + pub build_backend: Option, } #[derive(Default, Debug, Clone, PartialEq, Eq)] @@ -1684,3 +1685,44 @@ pub enum DependencyType { /// A dependency in `dependency-groups.{0}`. Group(GroupName), } + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(Serialize))] +pub struct BuildBackendSettingsSchema; + +impl<'de> Deserialize<'de> for BuildBackendSettingsSchema { + fn deserialize(_deserializer: D) -> Result + where + D: Deserializer<'de>, + { + Ok(BuildBackendSettingsSchema) + } +} + +#[cfg(feature = "schemars")] +impl schemars::JsonSchema for BuildBackendSettingsSchema { + fn schema_name() -> Cow<'static, str> { + BuildBackendSettings::schema_name() + } + + fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema { + BuildBackendSettings::json_schema(generator) + } +} + +impl OptionsMetadata for BuildBackendSettingsSchema { + fn record(visit: &mut dyn Visit) { + BuildBackendSettings::record(visit); + } + + fn documentation() -> Option<&'static str> { + BuildBackendSettings::documentation() + } + + fn metadata() -> OptionSet + where + Self: Sized + 'static, + { + BuildBackendSettings::metadata() + } +} diff --git a/crates/uv/tests/it/build_backend.rs b/crates/uv/tests/it/build_backend.rs index 3dd38278f..84b0ed8fe 100644 --- a/crates/uv/tests/it/build_backend.rs +++ b/crates/uv/tests/it/build_backend.rs @@ -858,3 +858,40 @@ fn symlinked_file() -> Result<()> { Ok(()) } + +/// Ignore invalid build backend settings when not building. +/// +/// They may be from another `uv_build` version that has a different schema. +#[test] +fn invalid_build_backend_settings_are_ignored() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! {r#" + [project] + name = "built-by-uv" + version = "0.1.0" + requires-python = ">=3.12" + + [tool.uv.build-backend] + # Error: `source-include` must be a list + source-include = "data/build-script.py" + + [build-system] + requires = ["uv_build>=10000,<10001"] + build-backend = "uv_build" + "#})?; + + // Since we are not building, this must pass without complaining about the error in + // `tool.uv.build-backend`. + uv_snapshot!(context.filters(), context.lock(), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + "); + + Ok(()) +} From 1c7c174bc80e9209bb52f975743c0cf55bc78e6d Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Mon, 30 Jun 2025 10:39:47 -0500 Subject: [PATCH 136/185] Include the canonical path in the interpreter query cache key (#14331) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This fixes an obscure cache collision in Python interpreter queries, which we believe to be the root cause of CI flakes we've been seeing where a project environment is invalidated and recreated. This work follows from the logs in [this CI run](https://github.com/astral-sh/uv/actions/runs/15934322410/job/44950599993?pr=14326) which captured one of the flakes with tracing enabled. There, we can see that the project environment is invalidated because the Python interpreter in the environment has a different version than expected: ``` DEBUG Checking for Python environment at `.venv` TRACE Cached interpreter info for Python 3.12.9, skipping probing: .venv/bin/python3 DEBUG The interpreter in the project environment has different version (3.12.9) than it was created with (3.9.21) ``` (this message is updated to reflect #14329) The flow is roughly: - We create an environment with 3.12.9 - We query the environment, and cache the interpreter version for `.venv/bin/python` - We create an environment for 3.9.12, replacing the existing one - We query the environment, and read the cached information The Python cache entries are keyed by the absolute path to the interpreter, and rely on the modification time (ctime, nsec resolution) of the canonicalized path to determine if the cache entry should be invalidated. The key is a hex representation of a u64 sea hasher output — which is very unlikely to collide. After an audit of the Python query caching logic, we determined that the most likely cause of a collision in cache entries is that the modification times of underlying interpreters are identical. This seems pretty feasible, especially if the file system does not support nanosecond precision — though it appears that the GitHub runners do support it. The fix here is to include the canonicalized path in the cache key, which ensures we're looking at the modification time of the _same_ underlying interpreter. This will "invalidate" all existing interpreter cache entries but that's not a big deal. This should also have the effect of reducing cache churn for interpreters in virtual environments. Now, when you change Python versions, we won't invalidate the previous cache entry so if you change _back_ to the old version we can re-use our cached information. It's a bit speculative, since we don't have a deterministic reproduction in CI, but this is the strongest candidate given the logs and should increase correctness regardless. Closes https://github.com/astral-sh/uv/issues/14160 Closes https://github.com/astral-sh/uv/issues/13744 Closes https://github.com/astral-sh/uv/issues/13745 Once it's confirmed the flakes are resolved, we should revert - https://github.com/astral-sh/uv/pull/14275 - #13817 --- crates/uv-python/src/interpreter.rs | 55 +++++++++++++++++------------ 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/crates/uv-python/src/interpreter.rs b/crates/uv-python/src/interpreter.rs index df27b497b..0f074ebb6 100644 --- a/crates/uv-python/src/interpreter.rs +++ b/crates/uv-python/src/interpreter.rs @@ -967,6 +967,31 @@ impl InterpreterInfo { pub(crate) fn query_cached(executable: &Path, cache: &Cache) -> Result { let absolute = std::path::absolute(executable)?; + // Provide a better error message if the link is broken or the file does not exist. Since + // `canonicalize_executable` does not resolve the file on Windows, we must re-use this logic + // for the subsequent metadata read as we may not have actually resolved the path. + let handle_io_error = |err: io::Error| -> Error { + if err.kind() == io::ErrorKind::NotFound { + // Check if it looks like a venv interpreter where the underlying Python + // installation was removed. + if absolute + .symlink_metadata() + .is_ok_and(|metadata| metadata.is_symlink()) + { + Error::BrokenSymlink(BrokenSymlink { + path: executable.to_path_buf(), + venv: uv_fs::is_virtualenv_executable(executable), + }) + } else { + Error::NotFound(executable.to_path_buf()) + } + } else { + err.into() + } + }; + + let canonical = canonicalize_executable(&absolute).map_err(handle_io_error)?; + let cache_entry = cache.entry( CacheBucket::Interpreter, // Shard interpreter metadata by host architecture, operating system, and version, to @@ -978,33 +1003,17 @@ impl InterpreterInfo { )), // We use the absolute path for the cache entry to avoid cache collisions for relative // paths. But we don't want to query the executable with symbolic links resolved because - // that can change reported values, e.g., `sys.executable`. - format!("{}.msgpack", cache_digest(&absolute)), + // that can change reported values, e.g., `sys.executable`. We include the canonical + // path in the cache entry as well, otherwise we can have cache collisions if an + // absolute path refers to different interpreters with matching ctimes, e.g., if you + // have a `.venv/bin/python` pointing to both Python 3.12 and Python 3.13 that were + // modified at the same time. + format!("{}.msgpack", cache_digest(&(&absolute, &canonical))), ); // We check the timestamp of the canonicalized executable to check if an underlying // interpreter has been modified. - let modified = canonicalize_executable(&absolute) - .and_then(Timestamp::from_path) - .map_err(|err| { - if err.kind() == io::ErrorKind::NotFound { - // Check if it looks like a venv interpreter where the underlying Python - // installation was removed. - if absolute - .symlink_metadata() - .is_ok_and(|metadata| metadata.is_symlink()) - { - Error::BrokenSymlink(BrokenSymlink { - path: executable.to_path_buf(), - venv: uv_fs::is_virtualenv_executable(executable), - }) - } else { - Error::NotFound(executable.to_path_buf()) - } - } else { - err.into() - } - })?; + let modified = Timestamp::from_path(canonical).map_err(handle_io_error)?; // Read from the cache. if cache From 317ce6e24551f05e6b78b0ce46c5afdeb0645b32 Mon Sep 17 00:00:00 2001 From: Aria Desires Date: Mon, 30 Jun 2025 17:42:00 -0400 Subject: [PATCH 137/185] disfavor aarch64 windows in its own house (#13724) and prefer emulated x64 windows in its stead. This is preparatory work for shipping support for uv downloading and installing aarch64 (arm64) windows Pythons. We've [had builds for this platform ready for a while](https://github.com/astral-sh/python-build-standalone/pull/387), but have held back on shipping them due to a fundamental problem: **The Python packaging ecosystem does not have strong support for aarch64 windows**, e.g., not many projects build aarch64 wheels yet. The net effect of this is that, if we handed you an aarch64 python interpreter on windows, you would have to build a lot more sdists, and there's a high chance you will simply fail to build that sdist and be sad. Yes unfortunately, in this case a non-native Python interpreter simply *works better* than the native one... in terms of working at all, today. Of course, if the native interpreter works for your project, it should presumably have better performance and platform compatibility. We do not want to stand in the way of progress, as ideally this situation is a temporary state of affairs as the ecosystem grows to support aarch64 windows. To enable progress, on aarch64 Windows builds of uv: * We will still use a native python interpreter, e.g., if it's at the front of your `PATH` or the only installed version. * If we are choosing between equally good interpreters that differ in architecture, x64 will be preferred. * If the aarch64 version is newer, we will prefer the aarch64 one. * We will emit a diagnostic on installation, and show the python request to pass to uv to force aarch64 windows to be used. * Will be shipping [aarch64 Windows Python downloads](https://github.com/astral-sh/python-build-standalone/pull/387) * Will probably add some kind of global override setting/env-var to disable this behaviour. * Will be shipping this behaviour in [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) We're coordinating with Microsoft, GitHub (for the `setup-python` action), and the CPython team (for the `python.org` installers), to ensure we're aligned on this default and the timing of toggling to prefer native distributions in the future. See discussion in - https://github.com/astral-sh/uv/issues/12906 --- This is an alternative to * #13719 which uses sorting rather than filtering, as discussed in * #13721 --- .github/workflows/ci.yml | 22 ++++++++++ crates/uv-python/src/platform.rs | 29 +++++++++++-- crates/uv/src/commands/python/install.rs | 53 +++++++++++++++++++++++- 3 files changed, 98 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 870171cdc..a0baa96c8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2090,6 +2090,28 @@ jobs: - name: "Validate global Python install" run: py -3.13 ./scripts/check_system_python.py --uv ./uv.exe + system-test-windows-aarch64-aarch64-python-313: + timeout-minutes: 10 + needs: build-binary-windows-aarch64 + name: "check system | aarch64 python3.13 on windows aarch64" + runs-on: github-windows-11-aarch64-4 + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: "3.13" + architecture: "arm64" + allow-prereleases: true + + - name: "Download binary" + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: uv-windows-aarch64-${{ github.sha }} + + - name: "Validate global Python install" + run: py -3.13 ./scripts/check_system_python.py --uv ./uv.exe + # Test our PEP 514 integration that installs Python into the Windows registry. system-test-windows-registry: timeout-minutes: 10 diff --git a/crates/uv-python/src/platform.rs b/crates/uv-python/src/platform.rs index 025592c37..ce8620ae2 100644 --- a/crates/uv-python/src/platform.rs +++ b/crates/uv-python/src/platform.rs @@ -43,15 +43,36 @@ impl Ord for Arch { return self.variant.cmp(&other.variant); } - let native = Arch::from_env(); + // For the time being, manually make aarch64 windows disfavored + // on its own host platform, because most packages don't have wheels for + // aarch64 windows, making emulation more useful than native execution! + // + // The reason we do this in "sorting" and not "supports" is so that we don't + // *refuse* to use an aarch64 windows pythons if they happen to be installed + // and nothing else is available. + // + // Similarly if someone manually requests an aarch64 windows install, we + // should respect that request (this is the way users should "override" + // this behaviour). + let preferred = if cfg!(all(windows, target_arch = "aarch64")) { + Arch { + family: target_lexicon::Architecture::X86_64, + variant: None, + } + } else { + // Prefer native architectures + Arch::from_env() + }; - // Prefer native architectures - match (self.family == native.family, other.family == native.family) { + match ( + self.family == preferred.family, + other.family == preferred.family, + ) { (true, true) => unreachable!(), (true, false) => std::cmp::Ordering::Less, (false, true) => std::cmp::Ordering::Greater, (false, false) => { - // Both non-native, fallback to lexicographic order + // Both non-preferred, fallback to lexicographic order self.family.to_string().cmp(&other.family.to_string()) } } diff --git a/crates/uv/src/commands/python/install.rs b/crates/uv/src/commands/python/install.rs index 08f95003e..3df0cf91d 100644 --- a/crates/uv/src/commands/python/install.rs +++ b/crates/uv/src/commands/python/install.rs @@ -1,4 +1,5 @@ use std::borrow::Cow; +use std::collections::BTreeMap; use std::fmt::Write; use std::io::ErrorKind; use std::path::{Path, PathBuf}; @@ -15,7 +16,9 @@ use tracing::{debug, trace}; use uv_configuration::PreviewMode; use uv_fs::Simplified; -use uv_python::downloads::{self, DownloadResult, ManagedPythonDownload, PythonDownloadRequest}; +use uv_python::downloads::{ + self, ArchRequest, DownloadResult, ManagedPythonDownload, PythonDownloadRequest, +}; use uv_python::managed::{ ManagedPythonInstallation, ManagedPythonInstallations, PythonMinorVersionLink, create_link_to_executable, python_executable_dir, @@ -401,6 +404,7 @@ pub(crate) async fn install( let mut errors = vec![]; let mut downloaded = Vec::with_capacity(downloads.len()); + let mut requests_by_new_installation = BTreeMap::new(); while let Some((download, result)) = tasks.next().await { match result { Ok(download_result) => { @@ -412,10 +416,19 @@ pub(crate) async fn install( let installation = ManagedPythonInstallation::new(path, download); changelog.installed.insert(installation.key().clone()); + for request in &requests { + // Take note of which installations satisfied which requests + if request.matches_installation(&installation) { + requests_by_new_installation + .entry(installation.key().clone()) + .or_insert(Vec::new()) + .push(request); + } + } if changelog.existing.contains(installation.key()) { changelog.uninstalled.insert(installation.key().clone()); } - downloaded.push(installation); + downloaded.push(installation.clone()); } Err(err) => { errors.push((download.key().clone(), anyhow::Error::new(err))); @@ -529,6 +542,42 @@ pub(crate) async fn install( } if !changelog.installed.is_empty() { + for install_key in &changelog.installed { + // Make a note if the selected python is non-native for the architecture, + // if none of the matching user requests were explicit + let native_arch = Arch::from_env(); + if install_key.arch().family() != native_arch.family() { + let not_explicit = + requests_by_new_installation + .get(install_key) + .and_then(|requests| { + let all_non_explicit = requests.iter().all(|request| { + if let PythonRequest::Key(key) = &request.request { + !matches!(key.arch(), Some(ArchRequest::Explicit(_))) + } else { + true + } + }); + if all_non_explicit { + requests.iter().next() + } else { + None + } + }); + if let Some(not_explicit) = not_explicit { + let native_request = + not_explicit.download_request.clone().with_arch(native_arch); + writeln!( + printer.stderr(), + "{} uv selected a Python distribution with an emulated architecture ({}) for your platform because support for the native architecture ({}) is not yet mature; to override this behaviour, request the native architecture explicitly with: {}", + "note:".bold(), + install_key.arch(), + native_arch, + native_request + )?; + } + } + } if changelog.installed.len() == 1 { let installed = changelog.installed.iter().next().unwrap(); // Ex) "Installed Python 3.9.7 in 1.68s" From 2f9061dcd08a797072bc806a5c736fd7be3acfe3 Mon Sep 17 00:00:00 2001 From: Aria Desires Date: Mon, 30 Jun 2025 18:02:19 -0400 Subject: [PATCH 138/185] Update python, add support for installing arm windows pythons (#14374) --- .github/workflows/ci.yml | 96 ++- crates/uv-python/download-metadata.json | 1008 +++++++++++++---------- 2 files changed, 645 insertions(+), 459 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a0baa96c8..6c6d47ce2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -852,7 +852,7 @@ jobs: timeout-minutes: 10 needs: build-binary-windows-aarch64 name: "smoke test | windows aarch64" - runs-on: github-windows-11-aarch64-4 + runs-on: windows-11-arm steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -1000,6 +1000,96 @@ jobs: ./uv run python -c "" ./uv run -p 3.13t python -c "" + integration-test-windows-aarch64-implicit: + timeout-minutes: 10 + needs: build-binary-windows-aarch64 + name: "integration test | aarch64 windows implicit" + runs-on: windows-11-arm + + steps: + - name: "Download binary" + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: uv-windows-aarch64-${{ github.sha }} + + - name: "Install Python via uv (implicitly select x64)" + run: | + ./uv python install -v 3.13 + + - name: "Create a virtual environment (stdlib)" + run: | + & (./uv python find 3.13) -m venv .venv + + - name: "Check version (stdlib)" + run: | + .venv/Scripts/python --version + + - name: "Create a virtual environment (uv)" + run: | + ./uv venv -p 3.13 --managed-python + + - name: "Check version (uv)" + run: | + .venv/Scripts/python --version + + - name: "Check is x64" + run: | + .venv/Scripts/python -c "import sys; exit(1) if 'AMD64' not in sys.version else exit(0)" + + - name: "Check install" + run: | + ./uv pip install -v anyio + + - name: "Check uv run" + run: | + ./uv run python -c "" + ./uv run -p 3.13 python -c "" + + integration-test-windows-aarch64-explicit: + timeout-minutes: 10 + needs: build-binary-windows-aarch64 + name: "integration test | aarch64 windows explicit" + runs-on: windows-11-arm + + steps: + - name: "Download binary" + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: uv-windows-aarch64-${{ github.sha }} + + - name: "Install Python via uv (explicitly select aarch64)" + run: | + ./uv python install -v cpython-3.13-windows-aarch64-none + + - name: "Create a virtual environment (stdlib)" + run: | + & (./uv python find 3.13) -m venv .venv + + - name: "Check version (stdlib)" + run: | + .venv/Scripts/python --version + + - name: "Create a virtual environment (uv)" + run: | + ./uv venv -p 3.13 --managed-python + + - name: "Check version (uv)" + run: | + .venv/Scripts/python --version + + - name: "Check is NOT x64" + run: | + .venv/Scripts/python -c "import sys; exit(1) if 'AMD64' in sys.version else exit(0)" + + - name: "Check install" + run: | + ./uv pip install -v anyio + + - name: "Check uv run" + run: | + ./uv run python -c "" + ./uv run -p 3.13 python -c "" + integration-test-pypy-linux: timeout-minutes: 10 needs: build-binary-linux-libc @@ -2072,7 +2162,7 @@ jobs: timeout-minutes: 10 needs: build-binary-windows-aarch64 name: "check system | x86-64 python3.13 on windows aarch64" - runs-on: github-windows-11-aarch64-4 + runs-on: windows-11-arm steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -2094,7 +2184,7 @@ jobs: timeout-minutes: 10 needs: build-binary-windows-aarch64 name: "check system | aarch64 python3.13 on windows aarch64" - runs-on: github-windows-11-aarch64-4 + runs-on: windows-11-arm steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 diff --git a/crates/uv-python/download-metadata.json b/crates/uv-python/download-metadata.json index 367efbd64..4035a6a48 100644 --- a/crates/uv-python/download-metadata.json +++ b/crates/uv-python/download-metadata.json @@ -11,8 +11,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-aarch64-apple-darwin-install_only_stripped.tar.gz", - "sha256": "93e38ced4feb2fe93d342d75918a1fb107949ce8378b6cb16c0fc78ab0c24d20", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-aarch64-apple-darwin-install_only_stripped.tar.gz", + "sha256": "56f9d76823f75e6853d23dc893dbe9c5e8b4d120ba1e568ceb62f7aff7285625", "variant": null }, "cpython-3.14.0b3-darwin-x86_64-none": { @@ -27,8 +27,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-x86_64-apple-darwin-install_only_stripped.tar.gz", - "sha256": "ea08efdd3b518139ed8b42aa5416e96e01b254aa78b409c6701163f2210fb37b", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-x86_64-apple-darwin-install_only_stripped.tar.gz", + "sha256": "135c7119d5ce56c97171769f51fee3be0e6ebfdab13c34dc28af46dbf00dffd1", "variant": null }, "cpython-3.14.0b3-linux-aarch64-gnu": { @@ -43,8 +43,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "3444de5a35a41a162df647eaec4d758eca966c4243ee22d0d1f8597edf48e5b8", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "046cfd0b5132d1ac441eaa81d20d4e71f113c0f4ca1831be35fc2581171a2e47", "variant": null }, "cpython-3.14.0b3-linux-armv7-gnueabi": { @@ -59,8 +59,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-armv7-unknown-linux-gnueabi-install_only_stripped.tar.gz", - "sha256": "ac1ce5cd8aa09f6584636e3bda2a14a8df46aaf435b73511cc0e57e237d77211", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-armv7-unknown-linux-gnueabi-install_only_stripped.tar.gz", + "sha256": "7922cceb24b1c7bbff5da8fa663ea3d75f0622cca862806d7e06080051d87919", "variant": null }, "cpython-3.14.0b3-linux-armv7-gnueabihf": { @@ -75,8 +75,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-armv7-unknown-linux-gnueabihf-install_only_stripped.tar.gz", - "sha256": "9058c9d70434693f5d6fa052c28dc7d7850e77efee6d32495d9d2605421b656d", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-armv7-unknown-linux-gnueabihf-install_only_stripped.tar.gz", + "sha256": "4b3450a8ae7f19c682563f44105f9af946bd5504f585fc135f78b066c11290b4", "variant": null }, "cpython-3.14.0b3-linux-powerpc64le-gnu": { @@ -91,8 +91,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-ppc64le-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "ad7e0d08638de2af6c70c13990b65d1bf412c95c28063de3252b186b48e5f29f", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-ppc64le-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "34c66ea418df6bff2f144a8317a8940827bf5545bb41e21ebf4ae991bc0d0fa7", "variant": null }, "cpython-3.14.0b3-linux-riscv64-gnu": { @@ -107,8 +107,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-riscv64-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "e95f10b21b149dc8fa59198d4cc0ff3720ec814aed9d03f33932e31cf8c17bf8", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-riscv64-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "952a785c85ae9b19ef5933bc406ed2ae622f535862f31f1d9d83579ebd7f1ab5", "variant": null }, "cpython-3.14.0b3-linux-s390x-gnu": { @@ -123,8 +123,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-s390x-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "7887516bf789785ce5cb7fa2eb2b4cffc1c454efe89a8eacfa6c66b2e054c9f8", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-s390x-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "09a6f67c1c8c1a056ac873c99b8dc9b393013fc06bee46b9a4f841686ac39948", "variant": null }, "cpython-3.14.0b3-linux-x86_64-gnu": { @@ -139,8 +139,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "014a2e7c96957cc3ac3925b8b1c06e6068cab3b20faf5f9f329f7e8d95d41bfd", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "771f78fb170b706c769742712143c6e3d0ed23e6b906bfd6e0bd7e5f253a8c15", "variant": null }, "cpython-3.14.0b3-linux-x86_64-musl": { @@ -155,8 +155,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-x86_64-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "72e15bb93e93cc4abcdd8ed44a666d12267ecd048d6e3b1bfeda3cc08187e659", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-x86_64-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "b5d645032acef89e5a112746bd0d7db5da2c04c72eebb5b7ca2dcd627c66502f", "variant": null }, "cpython-3.14.0b3-linux-x86_64_v2-gnu": { @@ -171,8 +171,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-x86_64_v2-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "fff629607727d50f04deca03056259f9aee607969fb2bf7db969587e53853302", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-x86_64_v2-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "bba4f53b8f9d93957bacecceb70e39199fe235836d5f7898f10c061fe4f21a19", "variant": null }, "cpython-3.14.0b3-linux-x86_64_v2-musl": { @@ -187,8 +187,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-x86_64_v2-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "acb6aeebf0c6bc6903600ce99d112754cc830a89224d0d63ef3c5c107f01685c", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-x86_64_v2-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "7692fedeff8c378c2fd046204296d4e9c921151176bcae80bff51d9cff6217d6", "variant": null }, "cpython-3.14.0b3-linux-x86_64_v3-gnu": { @@ -203,8 +203,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-x86_64_v3-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "df49f1a8e5c5aefc28ebc169fff77047878d0ae7fb7bf00221e10fc59f67f5fc", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-x86_64_v3-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "7d220528664f7b86555fed946ce29e9522db63b49f7c952c7df9bdee0a002466", "variant": null }, "cpython-3.14.0b3-linux-x86_64_v3-musl": { @@ -219,8 +219,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-x86_64_v3-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "d4672c060c5d8963af6355daaca40bb53938eee47a7346cab0259abff8e4f724", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-x86_64_v3-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "8188269487389fe6b1c441593257e5bd8bf6b8877a27ea0388d1a0282a2206f9", "variant": null }, "cpython-3.14.0b3-linux-x86_64_v4-gnu": { @@ -235,8 +235,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-x86_64_v4-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "925a47c3385ed2e2d498cd8f7a0a9434242b4b42d80ba45149d35da34bc9953b", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-x86_64_v4-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "da2699921d0b1af5527fbe7484534a5afbc40bfd50b2b1d4c296ced849ae3c3f", "variant": null }, "cpython-3.14.0b3-linux-x86_64_v4-musl": { @@ -251,8 +251,24 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-x86_64_v4-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "3e3b57bd9b3e821bc26107afaa29457ef8e96a50c8254ca89796feb9386928a9", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-x86_64_v4-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "059cab36f0a7fc71d3bf401ba73b41dc2df40b8f593e46c983cc5f2cd531ad19", + "variant": null + }, + "cpython-3.14.0b3-windows-aarch64-none": { + "name": "cpython", + "arch": { + "family": "aarch64", + "variant": null + }, + "os": "windows", + "libc": "none", + "major": 3, + "minor": 14, + "patch": 0, + "prerelease": "b3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-aarch64-pc-windows-msvc-install_only_stripped.tar.gz", + "sha256": "fa9c906275b89bf2068945394edb75be580ba8a3a868d647bcde2a3b958cf7ad", "variant": null }, "cpython-3.14.0b3-windows-i686-none": { @@ -267,8 +283,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-i686-pc-windows-msvc-install_only_stripped.tar.gz", - "sha256": "9009cd7a90b1ab3e7325278fbaa5363f1c160c92edef05be5c9d0a5c67ede59e", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-i686-pc-windows-msvc-install_only_stripped.tar.gz", + "sha256": "e1d5ae8b19492bd7520dd5d377740afcfebe222b5dc52452a69b06678df02deb", "variant": null }, "cpython-3.14.0b3-windows-x86_64-none": { @@ -283,8 +299,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-x86_64-pc-windows-msvc-install_only_stripped.tar.gz", - "sha256": "b50dd04ffb2fdc051964cc733ed624c5ea7cae85ec51060b1b97a406dd00c176", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-x86_64-pc-windows-msvc-install_only_stripped.tar.gz", + "sha256": "d01bb3382f9d4e7e956914df3c9efef6817aaf8915afa3a51f965a41d32bf8de", "variant": null }, "cpython-3.14.0b3+freethreaded-darwin-aarch64-none": { @@ -299,8 +315,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-aarch64-apple-darwin-freethreaded%2Bpgo%2Blto-full.tar.zst", - "sha256": "0ad27d76b4a5ebe3fac67bf928ea07bea5335fe6f0f33880277db73640a96df1", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-aarch64-apple-darwin-freethreaded%2Bpgo%2Blto-full.tar.zst", + "sha256": "475280f8fe3882e89cc4fa944a74dfaa5e4cfa5303b40803f4d782a08bf983a3", "variant": "freethreaded" }, "cpython-3.14.0b3+freethreaded-darwin-x86_64-none": { @@ -315,8 +331,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-x86_64-apple-darwin-freethreaded%2Bpgo%2Blto-full.tar.zst", - "sha256": "26e5c3e51de17455ed4c7f2b81702b175cf230728e4fdd93b3c426d21df09df2", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-x86_64-apple-darwin-freethreaded%2Bpgo%2Blto-full.tar.zst", + "sha256": "79f93e3f673400764f00f5eb5f9d7c7c8ed20889a9a05f068b1cdc45141821c5", "variant": "freethreaded" }, "cpython-3.14.0b3+freethreaded-linux-aarch64-gnu": { @@ -331,8 +347,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-aarch64-unknown-linux-gnu-freethreaded%2Blto-full.tar.zst", - "sha256": "15abb894679fafa47e71b37fb722de526cad5a55b42998d9ba7201023299631b", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-aarch64-unknown-linux-gnu-freethreaded%2Blto-full.tar.zst", + "sha256": "14ea3ce113f8eff97282c917c29dda2b2e9d9add07100b0e9b48377f1b5691cf", "variant": "freethreaded" }, "cpython-3.14.0b3+freethreaded-linux-armv7-gnueabi": { @@ -347,8 +363,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-armv7-unknown-linux-gnueabi-freethreaded%2Blto-full.tar.zst", - "sha256": "a2e1f3628beec26b88eb5d5139fcc95a296d163a5a02910337a6a309450e340f", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-armv7-unknown-linux-gnueabi-freethreaded%2Blto-full.tar.zst", + "sha256": "57d60ebe0a930faf07c7d2a59eadf3d5e59cf663492d0cadb91b4a936ec77319", "variant": "freethreaded" }, "cpython-3.14.0b3+freethreaded-linux-armv7-gnueabihf": { @@ -363,8 +379,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-armv7-unknown-linux-gnueabihf-freethreaded%2Blto-full.tar.zst", - "sha256": "30896f01c54a99e34d7d91a893568c32f99c597ecba8767ab83f501b770a3831", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-armv7-unknown-linux-gnueabihf-freethreaded%2Blto-full.tar.zst", + "sha256": "966e2ec436d34cfb5c485c8a45029de8c7ab2501e97717a361c10bdf464e2287", "variant": "freethreaded" }, "cpython-3.14.0b3+freethreaded-linux-powerpc64le-gnu": { @@ -379,8 +395,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-ppc64le-unknown-linux-gnu-freethreaded%2Blto-full.tar.zst", - "sha256": "2127b36c4b16da76a713fb4c2e8a6a2757a6d73a07a6ee4fa14d2a02633e0605", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-ppc64le-unknown-linux-gnu-freethreaded%2Blto-full.tar.zst", + "sha256": "9a036e9fff85e35dc7d9488604675bd139434e34020738b6e61a904e47567a50", "variant": "freethreaded" }, "cpython-3.14.0b3+freethreaded-linux-riscv64-gnu": { @@ -395,8 +411,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-riscv64-unknown-linux-gnu-freethreaded%2Blto-full.tar.zst", - "sha256": "dca86f8df4d6a65a69e8deb65e60ed0b27a376f2d677ec9150138d4e3601f4f7", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-riscv64-unknown-linux-gnu-freethreaded%2Blto-full.tar.zst", + "sha256": "875b77a12acf56f5b4349995271f387f0dd82e11c97b70d268070855b39b4bc4", "variant": "freethreaded" }, "cpython-3.14.0b3+freethreaded-linux-s390x-gnu": { @@ -411,8 +427,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-s390x-unknown-linux-gnu-freethreaded%2Blto-full.tar.zst", - "sha256": "5f119f34846d6f150c8de3b8ce81418f5cf60f90b51fcc594cb54d6ab4db030d", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-s390x-unknown-linux-gnu-freethreaded%2Blto-full.tar.zst", + "sha256": "328fb2c96de92f38c69f5031ed0dd72e4d317593ac13dde0c1daadb2e554317b", "variant": "freethreaded" }, "cpython-3.14.0b3+freethreaded-linux-x86_64-gnu": { @@ -427,8 +443,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-x86_64-unknown-linux-gnu-freethreaded%2Bpgo%2Blto-full.tar.zst", - "sha256": "ac5373d3b945298f34f1ebd5b03ce35ce92165638443ef65f8ca2d2eba07e39d", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-x86_64-unknown-linux-gnu-freethreaded%2Bpgo%2Blto-full.tar.zst", + "sha256": "fff6d39b2b03ea19fe191b1c64e67732b3f2d8f374bc6c6fd9bd94954d6287f2", "variant": "freethreaded" }, "cpython-3.14.0b3+freethreaded-linux-x86_64-musl": { @@ -443,8 +459,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-x86_64-unknown-linux-musl-freethreaded%2Blto-full.tar.zst", - "sha256": "e08e0202777269916295cf570e78bfb160250f88c20bd6f27fd1a72dcb03c8b9", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-x86_64-unknown-linux-musl-freethreaded%2Blto-full.tar.zst", + "sha256": "b227cf474974ea4a92bc2a633e288456403f564251bf07b1a1ae288e0fac5551", "variant": "freethreaded" }, "cpython-3.14.0b3+freethreaded-linux-x86_64_v2-gnu": { @@ -459,8 +475,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-x86_64_v2-unknown-linux-gnu-freethreaded%2Bpgo%2Blto-full.tar.zst", - "sha256": "5a36083ac0df9c72416a9cde665c6f86dfe78ebb348fc6a7b4b155ef0112fec9", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-x86_64_v2-unknown-linux-gnu-freethreaded%2Bpgo%2Blto-full.tar.zst", + "sha256": "b9b4a72b211d97cafb37b66cb4a918b72af24bb4d5b957a96e978cad22a5cd67", "variant": "freethreaded" }, "cpython-3.14.0b3+freethreaded-linux-x86_64_v2-musl": { @@ -475,8 +491,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-x86_64_v2-unknown-linux-musl-freethreaded%2Blto-full.tar.zst", - "sha256": "70297d1edad54eea78c8cd5174e21ab7e2ed2d754f512896ae0f4006517b101c", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-x86_64_v2-unknown-linux-musl-freethreaded%2Blto-full.tar.zst", + "sha256": "f7898473617e67d3d6fd428a8c340ae340eb043d35e1d1584ed0497b3b933cb6", "variant": "freethreaded" }, "cpython-3.14.0b3+freethreaded-linux-x86_64_v3-gnu": { @@ -491,8 +507,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-x86_64_v3-unknown-linux-gnu-freethreaded%2Bpgo%2Blto-full.tar.zst", - "sha256": "8cce11843eae8c78f0045227f7637c451943406aa04c1dc693ca7bf4874b2cbd", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-x86_64_v3-unknown-linux-gnu-freethreaded%2Bpgo%2Blto-full.tar.zst", + "sha256": "7844c24cec06402fd6e437c62c347ea37ff685e2e1fda0fbc3076a70b2963296", "variant": "freethreaded" }, "cpython-3.14.0b3+freethreaded-linux-x86_64_v3-musl": { @@ -507,8 +523,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-x86_64_v3-unknown-linux-musl-freethreaded%2Blto-full.tar.zst", - "sha256": "8da62b2823fb28d8d34093b20ac0d55034a47715afa35ed3a0fab49f2cc74e49", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-x86_64_v3-unknown-linux-musl-freethreaded%2Blto-full.tar.zst", + "sha256": "f39381f98e18a06fdc35137471f267d6de018bf0f6209e993a769c494b3e7fcd", "variant": "freethreaded" }, "cpython-3.14.0b3+freethreaded-linux-x86_64_v4-gnu": { @@ -523,8 +539,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-x86_64_v4-unknown-linux-gnu-freethreaded%2Bpgo%2Blto-full.tar.zst", - "sha256": "ded53979b407e350ad4c9e87646f9521762c0769aa26d9450ba02ec6801961a2", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-x86_64_v4-unknown-linux-gnu-freethreaded%2Bpgo%2Blto-full.tar.zst", + "sha256": "c19941d7c0d7002ebad3d13947ad0231215ed1d6a9e93c8318e71dbaf6a9bbdd", "variant": "freethreaded" }, "cpython-3.14.0b3+freethreaded-linux-x86_64_v4-musl": { @@ -539,8 +555,24 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-x86_64_v4-unknown-linux-musl-freethreaded%2Blto-full.tar.zst", - "sha256": "d90c08a393ee21fd9c4d3d7f4dcf5b14cb6251d3cb77df15cccc233e67fd85c1", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-x86_64_v4-unknown-linux-musl-freethreaded%2Blto-full.tar.zst", + "sha256": "e7d6c051de697ed6f95a0560fde3eb79c20b2ea9ca94ef02c4f7ec406f813ec4", + "variant": "freethreaded" + }, + "cpython-3.14.0b3+freethreaded-windows-aarch64-none": { + "name": "cpython", + "arch": { + "family": "aarch64", + "variant": null + }, + "os": "windows", + "libc": "none", + "major": 3, + "minor": 14, + "patch": 0, + "prerelease": "b3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-aarch64-pc-windows-msvc-freethreaded%2Bpgo-full.tar.zst", + "sha256": "986456a0e936f332a78a4130ea1114684d10b42b92b92d93b63da974767ae426", "variant": "freethreaded" }, "cpython-3.14.0b3+freethreaded-windows-i686-none": { @@ -555,8 +587,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-i686-pc-windows-msvc-freethreaded%2Bpgo-full.tar.zst", - "sha256": "97b59f1087c7f05c088a84998c0babf1b824358d28759e70c8090ec9a45e36aa", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-i686-pc-windows-msvc-freethreaded%2Bpgo-full.tar.zst", + "sha256": "dc630b0715cd1c7b23291715ce4fa62506ccd0578c4b186c5fd0a92a34cc6f0e", "variant": "freethreaded" }, "cpython-3.14.0b3+freethreaded-windows-x86_64-none": { @@ -571,8 +603,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-x86_64-pc-windows-msvc-freethreaded%2Bpgo-full.tar.zst", - "sha256": "5c864084d8b8d5b5e9d4d5005f92ec2f7bdb65c13bc9b95a9ac52b2bcb4db8e0", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-x86_64-pc-windows-msvc-freethreaded%2Bpgo-full.tar.zst", + "sha256": "d136ddc9fcf1f27e5791dd3dd5edf755830f1eef65066af749760f350cea41c8", "variant": "freethreaded" }, "cpython-3.14.0b3+debug-linux-aarch64-gnu": { @@ -587,8 +619,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-aarch64-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "6ad6bcadb2c1b862f3dd8c3a56c90eac2349a54dcf14628385d0d3bf5e993173", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-aarch64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "9bef0ad9e9c8c477c5a632074cb34d8ab3c25acaff95d7d7052dbabb952459c7", "variant": "debug" }, "cpython-3.14.0b3+debug-linux-armv7-gnueabi": { @@ -603,8 +635,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-armv7-unknown-linux-gnueabi-debug-full.tar.zst", - "sha256": "bd88ac4f905609bc5ee3ec6fc9d3482ce16f05b14052946aec3ed7f9ba8f83d2", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-armv7-unknown-linux-gnueabi-debug-full.tar.zst", + "sha256": "28324fddb6699ce11ae62dc6458e963c44e82f045550ced84ad383b12a810472", "variant": "debug" }, "cpython-3.14.0b3+debug-linux-armv7-gnueabihf": { @@ -619,8 +651,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-armv7-unknown-linux-gnueabihf-debug-full.tar.zst", - "sha256": "4d94559a5ae00bf849f5204a2f66eee119dd979cc0da8089edd1b57bce5a165f", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-armv7-unknown-linux-gnueabihf-debug-full.tar.zst", + "sha256": "74e896ddc8ec4b34f2f5c274132a96bb191b1524bc1627929084bec64ce2db62", "variant": "debug" }, "cpython-3.14.0b3+debug-linux-powerpc64le-gnu": { @@ -635,8 +667,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-ppc64le-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "11a6c51fe6c0e9dfc9fdd7439151b34f5e4896f82f1894fd1aee32052e5ca4d2", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-ppc64le-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "127450fe5cb01a313870aa32d97740b0a5b3eaac2119a1590cabf66409fb31a4", "variant": "debug" }, "cpython-3.14.0b3+debug-linux-riscv64-gnu": { @@ -651,8 +683,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-riscv64-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "4e84e69007d047e1f6a76ea894a8b919e82e7f86860525eb3a42a9cb30ce7044", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-riscv64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "7289b088a22731a0530c25395dd0617fe3d21fddc0c806206daa35b33d16f4ac", "variant": "debug" }, "cpython-3.14.0b3+debug-linux-s390x-gnu": { @@ -667,8 +699,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-s390x-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "f2f8dde10c119bbb3313a7ba4db59dd09124e283f2a65217f50504d18e9c511e", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-s390x-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "356b05d8d5c612bbd29acaed04dc3c54d8ea42af0d31608370b04cce5093c4e7", "variant": "debug" }, "cpython-3.14.0b3+debug-linux-x86_64-gnu": { @@ -683,8 +715,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-x86_64-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "26b5ad31f9902f271bc5e3e886975d242d4155ea43968e695214147b6f0758a3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-x86_64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "3092e467a21a4f00d030f0ac3efb97e49939debe3c00ed8c80cfd25c0db238f7", "variant": "debug" }, "cpython-3.14.0b3+debug-linux-x86_64-musl": { @@ -699,8 +731,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-x86_64-unknown-linux-musl-debug-full.tar.zst", - "sha256": "6c608708569a018db38e888410c7871b00ed9b5caa3dabf19ddc51e64e2200ab", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-x86_64-unknown-linux-musl-debug-full.tar.zst", + "sha256": "d8391e345823d2eaf0745ddb77982a35c032674e1d08ec29321568c21737a15e", "variant": "debug" }, "cpython-3.14.0b3+debug-linux-x86_64_v2-gnu": { @@ -715,8 +747,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "2499487ea8a5aa28c3ae5e9073e9cb7b14fdf45722d58db32ba20107a8ff4657", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "3f497ae10d574532c0a79b4704ba7944ee41586e86997ec1933c99efdfe19ec3", "variant": "debug" }, "cpython-3.14.0b3+debug-linux-x86_64_v2-musl": { @@ -731,8 +763,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-x86_64_v2-unknown-linux-musl-debug-full.tar.zst", - "sha256": "88233f533101941f03aef44bb58e44c6e9a2f7802ae85294ff3804472136d363", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-x86_64_v2-unknown-linux-musl-debug-full.tar.zst", + "sha256": "da154e3008bc4bd4185993bc04b355b79fbcd63a900b049d4595ea3615aef76c", "variant": "debug" }, "cpython-3.14.0b3+debug-linux-x86_64_v3-gnu": { @@ -747,8 +779,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "edd1800dd1d2abc38db383d7ff61bb21597f608a64ab44cc23f009af0291a96c", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "a64d5b16c684700b58cff51164c0faca6dc4de3dc6509ea958f975217fb1f804", "variant": "debug" }, "cpython-3.14.0b3+debug-linux-x86_64_v3-musl": { @@ -763,8 +795,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-x86_64_v3-unknown-linux-musl-debug-full.tar.zst", - "sha256": "6ceeb66c4876a7484b8ba9847e590d74ca35b67cbe692d06b3d3466a483406f8", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-x86_64_v3-unknown-linux-musl-debug-full.tar.zst", + "sha256": "ced306fbaf48996b422c5e45125de61578fe1a7b3f458bd4173f462b6b691fd2", "variant": "debug" }, "cpython-3.14.0b3+debug-linux-x86_64_v4-gnu": { @@ -779,8 +811,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "2f4d7ced306f9d4c800e9d7a86183c5f92c7858d8f64e38afd73db45414cbb82", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "68a7b564d38fafd2c0b864adcdc3595830d58714093951a5412ad72c021b302a", "variant": "debug" }, "cpython-3.14.0b3+debug-linux-x86_64_v4-musl": { @@ -795,8 +827,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.14.0b3%2B20250626-x86_64_v4-unknown-linux-musl-debug-full.tar.zst", - "sha256": "40191a40700aa65c0f92fcea8cd63de1d840ca5913c11b0226e677e1617dbf5d", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-x86_64_v4-unknown-linux-musl-debug-full.tar.zst", + "sha256": "7213fcbc7628c5a15cf3fb1c8bff43d0b8a5473fd86cf46941f7ee00f2b9d136", "variant": "debug" }, "cpython-3.14.0b2-darwin-aarch64-none": { @@ -5531,8 +5563,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-aarch64-apple-darwin-install_only_stripped.tar.gz", - "sha256": "2303b54d2780aeac8454b000515ea37c1ce9391dd0719bbf4f9aad453c4460fc", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-aarch64-apple-darwin-install_only_stripped.tar.gz", + "sha256": "a36fb4e711c2c7987d541aa5682db80fc7e7b72205bae6c330bf992d23db576f", "variant": null }, "cpython-3.13.5-darwin-x86_64-none": { @@ -5547,8 +5579,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-x86_64-apple-darwin-install_only_stripped.tar.gz", - "sha256": "baf14de314f191fd168fae778796c64e40667b28b8c78ae8b2bc340552e88a9a", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-x86_64-apple-darwin-install_only_stripped.tar.gz", + "sha256": "81b51eec47af8cd63d7fbcc809bd0ae3e07966a549620b159598525788961fdc", "variant": null }, "cpython-3.13.5-linux-aarch64-gnu": { @@ -5563,8 +5595,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "bef5e63e7c8c70c2336525ffd6758da801942127ce9f6c7c378fecc4ed09716d", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "906ffb000e53469921b0c3cfbcdab88e7fbf8a29cd48dec8cb9a9b8146947e1d", "variant": null }, "cpython-3.13.5-linux-armv7-gnueabi": { @@ -5579,8 +5611,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-armv7-unknown-linux-gnueabi-install_only_stripped.tar.gz", - "sha256": "ffa3f4b9a45bfac9e4ba045d2419354fccd2e716ffa191eccf0ec0d57af7ad8d", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-armv7-unknown-linux-gnueabi-install_only_stripped.tar.gz", + "sha256": "5cae766871606d74466e54fe8260ce81d74e1d545eb78da8d4986a910d8c418a", "variant": null }, "cpython-3.13.5-linux-armv7-gnueabihf": { @@ -5595,8 +5627,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-armv7-unknown-linux-gnueabihf-install_only_stripped.tar.gz", - "sha256": "f466eef0279076db5d929081dd287f98c7904fc713dd833a2ba6df4250a3b23e", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-armv7-unknown-linux-gnueabihf-install_only_stripped.tar.gz", + "sha256": "50e3cd7c633991c1b1d48a00a708ff002437f96e4b8400f93f97e237b2777208", "variant": null }, "cpython-3.13.5-linux-powerpc64le-gnu": { @@ -5611,8 +5643,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-ppc64le-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "3a1a91c3ac60188e27d87fb3487da1dd452bff984c9ed09c419d38c3c373eea7", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-ppc64le-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "5f2b7dbecf27e21afa5c3a6e203be0bd36a325b422b12bc3526de475e2566c3f", "variant": null }, "cpython-3.13.5-linux-riscv64-gnu": { @@ -5627,8 +5659,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-riscv64-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "8b5b76554004afec79469408f3facaa0407fac953c1e39badcd69fb4cbe9e7e3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-riscv64-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "713da45173f786f38e22f56d49b495647d93713dd9a0ec8bd8aa1f9e8e87ac1e", "variant": null }, "cpython-3.13.5-linux-s390x-gnu": { @@ -5643,8 +5675,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-s390x-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "9da68c953563b4ff3bf21a014350d76a20da5c5778ed433216f8c2ebc8bfd12b", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-s390x-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "c016223dcbfb5f3d686db67ba6d489426de21076d356640dcb0f8bf002573b43", "variant": null }, "cpython-3.13.5-linux-x86_64-gnu": { @@ -5659,8 +5691,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "1894edce20e8df3739d2136368a5c9f38f456f69c9ee4401c01fff4477e2934d", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "3057ed944be60b0d636aebfdde12dedb62c6554f33b5b50b8806dbc15a502324", "variant": null }, "cpython-3.13.5-linux-x86_64-musl": { @@ -5675,8 +5707,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-x86_64-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "8f50133eda9061154eb82a8807cb41ec64d2e17b429ddfcd1c90062e22d442fa", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-x86_64-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "e26d9d405c2f224ac6d989b82d99fd1e3468d2dfaf288ac5041f743fdea327c0", "variant": null }, "cpython-3.13.5-linux-x86_64_v2-gnu": { @@ -5691,8 +5723,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-x86_64_v2-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "bf77b2a07d8c91b27befc58de9e512340ccf7ee4a365c3cde6a59e28de8b3343", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-x86_64_v2-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "3041155088ef88fd28981f268de5b47d243ac9a2cffa3582fc4c1a6c633bb653", "variant": null }, "cpython-3.13.5-linux-x86_64_v2-musl": { @@ -5707,8 +5739,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-x86_64_v2-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "0e3347d77aa1fc36dd5b7a9dfc29968660c305bd2bb763af4abfab6a7f67a80d", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-x86_64_v2-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "1ec20ff2606e5510b30e0cb56ebe06b806b08c13712007d7550d26a35bddc9bd", "variant": null }, "cpython-3.13.5-linux-x86_64_v3-gnu": { @@ -5723,8 +5755,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-x86_64_v3-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "75347d44b5f7cf8c0642cb43b77797b15e6346633bc17c0fb47f7968d7221fa9", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-x86_64_v3-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "e62614725c2b31060063a5a54f24d01b90cd0f2418c6ff14c61cfad0f81cfefd", "variant": null }, "cpython-3.13.5-linux-x86_64_v3-musl": { @@ -5739,8 +5771,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-x86_64_v3-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "9e4191282a518d02db5d7ce5f42d7e96393768243f55e6a6a96e9c50f0cc72fa", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-x86_64_v3-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "9a02ef405e8d811c4cf703aa237c428f9460b4857465f6b4a5c23fce4948e887", "variant": null }, "cpython-3.13.5-linux-x86_64_v4-gnu": { @@ -5755,8 +5787,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-x86_64_v4-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "b841a8f77bedde2882f9bc65393939a49142c599b465c12c1bdd86dde52d6fc8", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-x86_64_v4-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "f06da893a220d6e97f845292c7623743b79167047c0d97ead6b2baa927d9d26b", "variant": null }, "cpython-3.13.5-linux-x86_64_v4-musl": { @@ -5771,8 +5803,24 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-x86_64_v4-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "446fb13abdcbf8a329074f9bfb4e9803b292a54f6252948edd08d5cf9c973289", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-x86_64_v4-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "a16a169ac9675e82ab190528e93cfbc7ab86f446485748afe1138010753eebd4", + "variant": null + }, + "cpython-3.13.5-windows-aarch64-none": { + "name": "cpython", + "arch": { + "family": "aarch64", + "variant": null + }, + "os": "windows", + "libc": "none", + "major": 3, + "minor": 13, + "patch": 5, + "prerelease": "", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-aarch64-pc-windows-msvc-install_only_stripped.tar.gz", + "sha256": "bc82935e2102d265b298fb1618930b36930fc1820d8dc2f6e3c2e34767917f44", "variant": null }, "cpython-3.13.5-windows-i686-none": { @@ -5787,8 +5835,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-i686-pc-windows-msvc-install_only_stripped.tar.gz", - "sha256": "c458f8200dd040af36d087791c48efa239160de641b7fda8d9c1fc6536a5e4a4", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-i686-pc-windows-msvc-install_only_stripped.tar.gz", + "sha256": "7ffe48c8242d24b029ae880a0651076ec3eb9b2a23a71f9ad170bee37cc40f42", "variant": null }, "cpython-3.13.5-windows-x86_64-none": { @@ -5803,8 +5851,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-x86_64-pc-windows-msvc-install_only_stripped.tar.gz", - "sha256": "ab9b81bb06a51791040f6a7e9bffa62eec7ae60041d3cf7409ee7431f2237c7b", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-x86_64-pc-windows-msvc-install_only_stripped.tar.gz", + "sha256": "5f7b96c86d4bea7a823d0fbce8e139a39acad3560bb0b8ffaae236c0bf8251f9", "variant": null }, "cpython-3.13.5+freethreaded-darwin-aarch64-none": { @@ -5819,8 +5867,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-aarch64-apple-darwin-freethreaded%2Bpgo%2Blto-full.tar.zst", - "sha256": "7223a0e13d5e290fa8441b5439d08fca6fe389bcc186f918f2edd808027dcd08", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-aarch64-apple-darwin-freethreaded%2Bpgo%2Blto-full.tar.zst", + "sha256": "71772559da8e7155ae34ee9e625126bb76df42d66184b533f82c74b7acd0b9f0", "variant": "freethreaded" }, "cpython-3.13.5+freethreaded-darwin-x86_64-none": { @@ -5835,8 +5883,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-x86_64-apple-darwin-freethreaded%2Bpgo%2Blto-full.tar.zst", - "sha256": "869ca9d095f9e8f50fc8609d55d6a937c48a7d0b09e7ab5a3679307f9eb90c70", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-x86_64-apple-darwin-freethreaded%2Bpgo%2Blto-full.tar.zst", + "sha256": "e31c161b78c141e9fde65e7a37ac0e944ac2f8fb0e001b49244deae824ed0343", "variant": "freethreaded" }, "cpython-3.13.5+freethreaded-linux-aarch64-gnu": { @@ -5851,8 +5899,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-aarch64-unknown-linux-gnu-freethreaded%2Blto-full.tar.zst", - "sha256": "8437225a6066e9f57a2ce631a73eceedffeadfe4146b7861e6ace5647a0472da", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-aarch64-unknown-linux-gnu-freethreaded%2Blto-full.tar.zst", + "sha256": "578fcc7c2f8e9816ccd7302a5f06f3b295e1059249f594b70a15eb6b744405e9", "variant": "freethreaded" }, "cpython-3.13.5+freethreaded-linux-armv7-gnueabi": { @@ -5867,8 +5915,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-armv7-unknown-linux-gnueabi-freethreaded%2Blto-full.tar.zst", - "sha256": "eabb70dff454bf923a728853c840ee318bc5a0061d988900f225de5b1eb4058b", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-armv7-unknown-linux-gnueabi-freethreaded%2Blto-full.tar.zst", + "sha256": "60be2a6390408fe4ff1def8b5841240749f6e036be0719aa4120dc6412657467", "variant": "freethreaded" }, "cpython-3.13.5+freethreaded-linux-armv7-gnueabihf": { @@ -5883,8 +5931,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-armv7-unknown-linux-gnueabihf-freethreaded%2Blto-full.tar.zst", - "sha256": "aa49a863343cbbae5a7a1104adb11e9d1d26843598eb5ba9e3db135d27df721a", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-armv7-unknown-linux-gnueabihf-freethreaded%2Blto-full.tar.zst", + "sha256": "e477210f0b1c66e3811433bb1d45d7336e9e0bb52f4ec2a4d6a789dbdd9de9c2", "variant": "freethreaded" }, "cpython-3.13.5+freethreaded-linux-powerpc64le-gnu": { @@ -5899,8 +5947,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-ppc64le-unknown-linux-gnu-freethreaded%2Blto-full.tar.zst", - "sha256": "09008067d69b833b831cc6090edb221f1cce780c4586db8231dcfb988d1b7571", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-ppc64le-unknown-linux-gnu-freethreaded%2Blto-full.tar.zst", + "sha256": "4d00d210cf083c83950e5af59df044c34538c15f823a6b07c667df2479bbcb21", "variant": "freethreaded" }, "cpython-3.13.5+freethreaded-linux-riscv64-gnu": { @@ -5915,8 +5963,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-riscv64-unknown-linux-gnu-freethreaded%2Blto-full.tar.zst", - "sha256": "3bc92a4057557e9a9f7e8bd8e673dfae54f9abbd14217ae4d986ba29c8c1e761", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-riscv64-unknown-linux-gnu-freethreaded%2Blto-full.tar.zst", + "sha256": "63408df03d6d6ec22a9d4e01701a1519247ceb35804161d64f0aed0d73154087", "variant": "freethreaded" }, "cpython-3.13.5+freethreaded-linux-s390x-gnu": { @@ -5931,8 +5979,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-s390x-unknown-linux-gnu-freethreaded%2Blto-full.tar.zst", - "sha256": "3206aa76d604d87222ef1cd069b4c7428b3a8f991580504ae21f0926c53a97c5", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-s390x-unknown-linux-gnu-freethreaded%2Blto-full.tar.zst", + "sha256": "af58974a23754b02301043d6e810fb3ce533d1c34c23d197a6690f3eb2f00fe1", "variant": "freethreaded" }, "cpython-3.13.5+freethreaded-linux-x86_64-gnu": { @@ -5947,8 +5995,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-x86_64-unknown-linux-gnu-freethreaded%2Bpgo%2Blto-full.tar.zst", - "sha256": "a45ffc5a812c3b6db1dce34fc72c35fb3c791075c4602d0fb742c889bc6bf26d", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-x86_64-unknown-linux-gnu-freethreaded%2Bpgo%2Blto-full.tar.zst", + "sha256": "976133de595393596207fa3c943d4e8e43ddb5d51b864766fc666a1752963e04", "variant": "freethreaded" }, "cpython-3.13.5+freethreaded-linux-x86_64-musl": { @@ -5963,8 +6011,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-x86_64-unknown-linux-musl-freethreaded%2Blto-full.tar.zst", - "sha256": "c0199c9816dea06a4bc6c5f970d4909660302acb62639a54da7b5d6a4bb45106", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-x86_64-unknown-linux-musl-freethreaded%2Blto-full.tar.zst", + "sha256": "f11c199765a4c96e93bd5ffc65477dc7eb4c4edd03f20ecf4d51ea0afc3be0c2", "variant": "freethreaded" }, "cpython-3.13.5+freethreaded-linux-x86_64_v2-gnu": { @@ -5979,8 +6027,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-x86_64_v2-unknown-linux-gnu-freethreaded%2Bpgo%2Blto-full.tar.zst", - "sha256": "5ab68785f2b5098e5e984bc49b9df1530bee0097dddcd4fe5f197e48d3e619f2", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-x86_64_v2-unknown-linux-gnu-freethreaded%2Bpgo%2Blto-full.tar.zst", + "sha256": "ff8066512111ac80962bdd094043f600eb9dbdd35e93f059f5ab34f9c4ff412e", "variant": "freethreaded" }, "cpython-3.13.5+freethreaded-linux-x86_64_v2-musl": { @@ -5995,8 +6043,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-x86_64_v2-unknown-linux-musl-freethreaded%2Blto-full.tar.zst", - "sha256": "b91e0b88eb9295fae197f870f2291a3bd1d47758f2aabc8c2e1996af1b14f180", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-x86_64_v2-unknown-linux-musl-freethreaded%2Blto-full.tar.zst", + "sha256": "26ab90c03672187ae4a330e59bf5142cbe7ed24e576edfd3b869fa108a3b3fc7", "variant": "freethreaded" }, "cpython-3.13.5+freethreaded-linux-x86_64_v3-gnu": { @@ -6011,8 +6059,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-x86_64_v3-unknown-linux-gnu-freethreaded%2Bpgo%2Blto-full.tar.zst", - "sha256": "91bdb8c0792ef29cef987b3198e18f9c441794b33f1266c9155530ddfcfa8b3a", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-x86_64_v3-unknown-linux-gnu-freethreaded%2Bpgo%2Blto-full.tar.zst", + "sha256": "15e614a96bf2d789500c9bee03c7330e61278c1b9b4f3138f38bfa05bbe4bd68", "variant": "freethreaded" }, "cpython-3.13.5+freethreaded-linux-x86_64_v3-musl": { @@ -6027,8 +6075,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-x86_64_v3-unknown-linux-musl-freethreaded%2Blto-full.tar.zst", - "sha256": "01191f5268159e7448a320703c40508802baa1780e855771311f01c550d80b58", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-x86_64_v3-unknown-linux-musl-freethreaded%2Blto-full.tar.zst", + "sha256": "448f46715607f34cc217ce2a51d0782417d99077b5ecd8330d2c53b74e893a0c", "variant": "freethreaded" }, "cpython-3.13.5+freethreaded-linux-x86_64_v4-gnu": { @@ -6043,8 +6091,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-x86_64_v4-unknown-linux-gnu-freethreaded%2Bpgo%2Blto-full.tar.zst", - "sha256": "ebcd45022331fe1ebdee98a3b16c876d8be147079206fa3ccc4d81b89fd7ac8b", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-x86_64_v4-unknown-linux-gnu-freethreaded%2Bpgo%2Blto-full.tar.zst", + "sha256": "6620f3bbd38732ce232d84abbad1836400a680a1208916af502bbeebc8ae825d", "variant": "freethreaded" }, "cpython-3.13.5+freethreaded-linux-x86_64_v4-musl": { @@ -6059,8 +6107,24 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-x86_64_v4-unknown-linux-musl-freethreaded%2Blto-full.tar.zst", - "sha256": "d30c596a8116a92f860d38b5d5a11e6dd5dea2bbbcdf7439e3223e357298b838", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-x86_64_v4-unknown-linux-musl-freethreaded%2Blto-full.tar.zst", + "sha256": "895e650967c266182f64c8f3a3fd395d336bbd95218fd0c7affc82f3c9465899", + "variant": "freethreaded" + }, + "cpython-3.13.5+freethreaded-windows-aarch64-none": { + "name": "cpython", + "arch": { + "family": "aarch64", + "variant": null + }, + "os": "windows", + "libc": "none", + "major": 3, + "minor": 13, + "patch": 5, + "prerelease": "", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-aarch64-pc-windows-msvc-freethreaded%2Bpgo-full.tar.zst", + "sha256": "4dc24bdf390356281dd23a4644565e7f49187894015b8292c0257a0f5cd2fd43", "variant": "freethreaded" }, "cpython-3.13.5+freethreaded-windows-i686-none": { @@ -6075,8 +6139,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-i686-pc-windows-msvc-freethreaded%2Bpgo-full.tar.zst", - "sha256": "d1d421904aa75fce2fb5cb59bacb83787fbe7fbc68dc355c7fdbd19094030473", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-i686-pc-windows-msvc-freethreaded%2Bpgo-full.tar.zst", + "sha256": "009f56db3de1351dd616fb1fd8b04343637dd84b01c1eb37af8559fa87c6b423", "variant": "freethreaded" }, "cpython-3.13.5+freethreaded-windows-x86_64-none": { @@ -6091,8 +6155,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-x86_64-pc-windows-msvc-freethreaded%2Bpgo-full.tar.zst", - "sha256": "79c5594d758c7db8323abc23325e17955a6c6e300fec04abdeecf29632de1e34", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-x86_64-pc-windows-msvc-freethreaded%2Bpgo-full.tar.zst", + "sha256": "7387f13148676858b9ab61ff7f03b82b88d81d0342346d1a156233b9478f3c81", "variant": "freethreaded" }, "cpython-3.13.5+debug-linux-aarch64-gnu": { @@ -6107,8 +6171,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-aarch64-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "bbc5dab19b3444b8056e717babc81d948b4a45b8f1a6e25d1c193fcaf572bf25", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-aarch64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "0698a989ead503f3181118a8c2793b431707190a1ef82fe2a5386a268ebe58d6", "variant": "debug" }, "cpython-3.13.5+debug-linux-armv7-gnueabi": { @@ -6123,8 +6187,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-armv7-unknown-linux-gnueabi-debug-full.tar.zst", - "sha256": "2be85ab8daa6c898292789d7fe83d939090a44681b0424789fba278a7dded5fd", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-armv7-unknown-linux-gnueabi-debug-full.tar.zst", + "sha256": "0a5d4cec664ede542ad92115f2c90318bed8020d093ec98a9cf935374d0b684e", "variant": "debug" }, "cpython-3.13.5+debug-linux-armv7-gnueabihf": { @@ -6139,8 +6203,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-armv7-unknown-linux-gnueabihf-debug-full.tar.zst", - "sha256": "372ca41f12f67a44d0aedd28714daade9d35b4c14157ea4cdd5b12eea3f5ddf8", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-armv7-unknown-linux-gnueabihf-debug-full.tar.zst", + "sha256": "6be78a4c8df0a4ee420e2a51b6f5581d7edf960b38bcf38c1a4f3ecee018b248", "variant": "debug" }, "cpython-3.13.5+debug-linux-powerpc64le-gnu": { @@ -6155,8 +6219,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-ppc64le-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "0cb23ab284eab18f471716f4aa1ba4ee570a75a6d79fa5cd091264c514e54a91", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-ppc64le-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "bba8b2e8c0a6095460c519c273d7e101c616c96e2992407301018d00a5d7b686", "variant": "debug" }, "cpython-3.13.5+debug-linux-riscv64-gnu": { @@ -6171,8 +6235,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-riscv64-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "315184f457d26e6e20cf69ec169f3fdfdd232e6766a29905cbb6044501fcd4e5", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-riscv64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "10be77859e5f99a83c07e4a521776d2a8860dd46ce8a46c1e3c2544d2593702a", "variant": "debug" }, "cpython-3.13.5+debug-linux-s390x-gnu": { @@ -6187,8 +6251,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-s390x-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "8bc2ca17f35f14b6f07882e75c7034263e23fbe003e275e068f2d84e823ed2bf", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-s390x-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "05e668c47ba1c75f0df0c01b6afb3af39ae9d17a90d02f7f6508c094de79aa3c", "variant": "debug" }, "cpython-3.13.5+debug-linux-x86_64-gnu": { @@ -6203,8 +6267,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-x86_64-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "994e77601b9c4f9f48000f575e1d1c640b8b3d795fb65b6b4656b7664df2f95c", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-x86_64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "5d8f83f26f8f203eb888ae42e02d6f85c26ac0306d65ad311e79ca2120095ce1", "variant": "debug" }, "cpython-3.13.5+debug-linux-x86_64-musl": { @@ -6219,8 +6283,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-x86_64-unknown-linux-musl-debug-full.tar.zst", - "sha256": "50027e1a8a82d7924993b49941c6c6fdeeee282cb31807021c44459637b1ca1e", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-x86_64-unknown-linux-musl-debug-full.tar.zst", + "sha256": "70705fd017908822cbfad83b444f4fc46301467f13e17c97de8e456ee9f08173", "variant": "debug" }, "cpython-3.13.5+debug-linux-x86_64_v2-gnu": { @@ -6235,8 +6299,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "e9c3d4fdaabe465969533d0129966957088a609814f5f909e25b1397d4fbe960", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "064e7eb23cce3b5ceb22d974d14c80a0ffbe309178d60d56ee910deda69c3bb2", "variant": "debug" }, "cpython-3.13.5+debug-linux-x86_64_v2-musl": { @@ -6251,8 +6315,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-x86_64_v2-unknown-linux-musl-debug-full.tar.zst", - "sha256": "1e16597f1d1a72a9ffde00d9d76eda5fdd269d3c69d40b1ab1ccb0ee2e2aafcd", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-x86_64_v2-unknown-linux-musl-debug-full.tar.zst", + "sha256": "f875a778f5c1f4a3f8c0c37ebdd2437eea9abcfa3eff4d161ee8d761d3018892", "variant": "debug" }, "cpython-3.13.5+debug-linux-x86_64_v3-gnu": { @@ -6267,8 +6331,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "ab9f5380fe0f3d34bb6c02d29103adf2d3b21f47a5f08613fe4f1d7b69d708b4", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "7024d12c2241595cb3c66a0c291d416cc431198aa01d8c2d642fad3266b4d2c8", "variant": "debug" }, "cpython-3.13.5+debug-linux-x86_64_v3-musl": { @@ -6283,8 +6347,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-x86_64_v3-unknown-linux-musl-debug-full.tar.zst", - "sha256": "11d126eb8cf367b904982d3b7581bd9bf77630774a1c68bb3290c2372f01360c", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-x86_64_v3-unknown-linux-musl-debug-full.tar.zst", + "sha256": "36ce0d9ca8163798c83d027d0abee2ed78ae0a91df96346ed5b7c26700131f38", "variant": "debug" }, "cpython-3.13.5+debug-linux-x86_64_v4-gnu": { @@ -6299,8 +6363,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "8f0bf5fdfd697d5fdd85f9beca4937e9cf93e213d27f46d782d35c30ca2876f6", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "31c2c46f031fd32b2a8955a9193365f8e56f06e4b598b72d34441b4461ca55e7", "variant": "debug" }, "cpython-3.13.5+debug-linux-x86_64_v4-musl": { @@ -6315,8 +6379,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.13.5%2B20250626-x86_64_v4-unknown-linux-musl-debug-full.tar.zst", - "sha256": "6fd687f89251c13df002d6bff0f57cbe199f724679df3b8de9bbabafb735d47b", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-x86_64_v4-unknown-linux-musl-debug-full.tar.zst", + "sha256": "1129ba59de94be77f3ed9d4fccb5f48f9f297600b71aeea79d20f170f84b4323", "variant": "debug" }, "cpython-3.13.4-darwin-aarch64-none": { @@ -10539,8 +10603,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-aarch64-apple-darwin-install_only_stripped.tar.gz", - "sha256": "4919401e556a2720e2fd75635485ef5a6bb4caedcaa228b49acdd060c1b89f4e", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-aarch64-apple-darwin-install_only_stripped.tar.gz", + "sha256": "52f103b1bae4105c8c3ba415fbfb992fc80672795c58262b96719efde97c49d9", "variant": null }, "cpython-3.12.11-darwin-x86_64-none": { @@ -10555,8 +10619,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-x86_64-apple-darwin-install_only_stripped.tar.gz", - "sha256": "4d5d672de6a6f2048f875ea0e6b02c9a4a0248d3d57271c740a1b877442552a1", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-x86_64-apple-darwin-install_only_stripped.tar.gz", + "sha256": "5e7229d72ec588b77f753ee043fcefe62f89190e84b8b43d20b1be4b4e6e8540", "variant": null }, "cpython-3.12.11-linux-aarch64-gnu": { @@ -10571,8 +10635,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "e6797c50c9ce77904e63b8c15cc97a9ff459006bea726bf2ce41ba2ef317d4af", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "117b1d2daa96486005827dd50029fecd61e99141396583ea8e57733f7cf0abad", "variant": null }, "cpython-3.12.11-linux-armv7-gnueabi": { @@ -10587,8 +10651,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-armv7-unknown-linux-gnueabi-install_only_stripped.tar.gz", - "sha256": "60c8843e9e70610f3d82f26b0ba395312382d36e3078d141896ab4aabd422be8", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-armv7-unknown-linux-gnueabi-install_only_stripped.tar.gz", + "sha256": "878d29bab42a2911927948636a1bdd1e83ac1d31dbe4b857a214b04eed2046de", "variant": null }, "cpython-3.12.11-linux-armv7-gnueabihf": { @@ -10603,8 +10667,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-armv7-unknown-linux-gnueabihf-install_only_stripped.tar.gz", - "sha256": "d97c4bcb8514bf2f7b202e018a2b99e9a4ab797bf1b0c31990641fe9652cae2e", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-armv7-unknown-linux-gnueabihf-install_only_stripped.tar.gz", + "sha256": "8a3ae17b0ecaa0334ca1bbd3d2cc7ea16d43ead269c03cdaedee54bfd6949962", "variant": null }, "cpython-3.12.11-linux-powerpc64le-gnu": { @@ -10619,8 +10683,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-ppc64le-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "bebc3aa45ff2d228f7378faedf2556a681e9c626b8e237d39cf4eb6552438242", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-ppc64le-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "bb0689d5101a4a85eaca7e8443912d196519103695937fb8e4fc819d66e48167", "variant": null }, "cpython-3.12.11-linux-riscv64-gnu": { @@ -10635,8 +10699,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-riscv64-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "e82a07c281bef00d14a6bf65b644a4758ee0c8105e0aa549c49471cc4656046f", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-riscv64-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "59323d58c55f3afc90d93d185f57bdfc9bbfcf1e713290df6eda91fbabf91a90", "variant": null }, "cpython-3.12.11-linux-s390x-gnu": { @@ -10651,8 +10715,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-s390x-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "ccd091fd9a8cc1a57da15d1e8ae523b6363c2ce9a0599ac1b63f5c8b70767268", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-s390x-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "925c52b00a55b52492c197ebc7499317e4403cf603f1670a6e655b9426d286f2", "variant": null }, "cpython-3.12.11-linux-x86_64-gnu": { @@ -10667,8 +10731,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "4ab7c1866d0b072b75d5d7b010f73e6539309c611ecad55852411fc8b0b44541", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "358ad7bf46260a2e1b31726adc62dc04258c7ea7469352e155ffdea0f168499e", "variant": null }, "cpython-3.12.11-linux-x86_64-musl": { @@ -10683,8 +10747,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-x86_64-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "2fe4faa95b605a4616c3c3d986fb8ce9a31c16a68b6e77f56c68468a87dff29e", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-x86_64-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "63bcf41a095c298d412efdb9ceba0009e8544809fe0fa9b1ba51dc918f3dc3ee", "variant": null }, "cpython-3.12.11-linux-x86_64_v2-gnu": { @@ -10699,8 +10763,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-x86_64_v2-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "531c60132607239f677d6bb91d33645cd7564f1f563e43fff889c67589d6e092", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-x86_64_v2-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "49de0e676faa6e72a988ebe41d67cd433d074ab4fd3b555935d0d4f7437f7168", "variant": null }, "cpython-3.12.11-linux-x86_64_v2-musl": { @@ -10715,8 +10779,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-x86_64_v2-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "c2a86e3077ccda859a8669896a4c41ea167c03b105c4307523e6e0b0bc820c25", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-x86_64_v2-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "66f322f7eb520a441ef341b166e9cbd45df83e5841fd4998181f431f719f5a8c", "variant": null }, "cpython-3.12.11-linux-x86_64_v3-gnu": { @@ -10731,8 +10795,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-x86_64_v3-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "07015ad91ec3ee89c410b09bae84a5910037766783ae47bfb09469f80e1f2337", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-x86_64_v3-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "be9ba63f7905b925d958aebcb8fcec40a2ba94552e2bd3d91818d97fc3d68ecb", "variant": null }, "cpython-3.12.11-linux-x86_64_v3-musl": { @@ -10747,8 +10811,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-x86_64_v3-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "891ba6c18dd688cf7b633ff2bb40aecf007d439c6732b2a9b8de78ffbb0b9421", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-x86_64_v3-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "598661a23fd5c0f2870c99194938732a3e1a4909e41b8a54401b821f1594e66b", "variant": null }, "cpython-3.12.11-linux-x86_64_v4-gnu": { @@ -10763,8 +10827,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-x86_64_v4-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "b7f5456cb566bf05bf1f1818eb2dda4f76a3c6257697f31d01ada063ec324f96", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-x86_64_v4-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "b76430b697fa0c1ef1b2b04361decf36b7108da719c48ca2098e6aa0cd9a7130", "variant": null }, "cpython-3.12.11-linux-x86_64_v4-musl": { @@ -10779,8 +10843,24 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-x86_64_v4-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "55f5121910e7c9229890e59407dc447807bee7173dc979af7cab8ea6ddd36881", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-x86_64_v4-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "88121676a946fe506d24821fc89439bc79a89a4eea9ffb5c903e9d7bc1c427d3", + "variant": null + }, + "cpython-3.12.11-windows-aarch64-none": { + "name": "cpython", + "arch": { + "family": "aarch64", + "variant": null + }, + "os": "windows", + "libc": "none", + "major": 3, + "minor": 12, + "patch": 11, + "prerelease": "", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-aarch64-pc-windows-msvc-install_only_stripped.tar.gz", + "sha256": "45e121ad8e82eb9204cc1da53d0fdf125923f7caed18bf282b3c00dcfae89486", "variant": null }, "cpython-3.12.11-windows-i686-none": { @@ -10795,8 +10875,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-i686-pc-windows-msvc-install_only_stripped.tar.gz", - "sha256": "9ddbc2d11445007c8c44ecc8fb23b60c19143e7ff2bc99f9a6d7ab19cf18825e", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-i686-pc-windows-msvc-install_only_stripped.tar.gz", + "sha256": "d80c67dd88aca4c94a57a8b2fe63de524a9e1f2308e5ecd2ca83a7961b9b9b17", "variant": null }, "cpython-3.12.11-windows-x86_64-none": { @@ -10811,8 +10891,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-x86_64-pc-windows-msvc-install_only_stripped.tar.gz", - "sha256": "429e1cc78f4b7591b4ec2063d334dd0bb85afb41e246af1e2b919acdf99fc1b0", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-x86_64-pc-windows-msvc-install_only_stripped.tar.gz", + "sha256": "91f7196d5d103eb3d1214f74606144adb326f0770d80c636cb11b96670477736", "variant": null }, "cpython-3.12.11+debug-linux-aarch64-gnu": { @@ -10827,8 +10907,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-aarch64-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "bc8c87d783ae3685df5a0954da4de6513cd0e82135c115c7d472adbabb9c992d", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-aarch64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "70a49e31e1f15036715e67ec08ef63df321171c7b922f16db9b462ed9d1ccbc6", "variant": "debug" }, "cpython-3.12.11+debug-linux-armv7-gnueabi": { @@ -10843,8 +10923,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-armv7-unknown-linux-gnueabi-debug-full.tar.zst", - "sha256": "40f70903123cf16fb677190388f1a63e45458c2b2ae106c8ddb3ef32ace4c8d1", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-armv7-unknown-linux-gnueabi-debug-full.tar.zst", + "sha256": "87f27b3efb66a226f7a03029f14cbabdc512974f8489b003cb848dec3fde869c", "variant": "debug" }, "cpython-3.12.11+debug-linux-armv7-gnueabihf": { @@ -10859,8 +10939,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-armv7-unknown-linux-gnueabihf-debug-full.tar.zst", - "sha256": "fec4a3348980ecbae93e241946bd825f57f26b5a1b99cc56ee4e6d4a3dd53f3c", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-armv7-unknown-linux-gnueabihf-debug-full.tar.zst", + "sha256": "28a431937f2b5467a0e74425d64bf27ca2e0c37681e3e93f71c19386cf457237", "variant": "debug" }, "cpython-3.12.11+debug-linux-powerpc64le-gnu": { @@ -10875,8 +10955,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-ppc64le-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "dea804e42c48de02e12fda2726dea59aa05091c792a2defe0c42637658711c46", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-ppc64le-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "e08f6f7faabda320f0a38a3388056f29641d7375a6247a6aebf885907a3baf2b", "variant": "debug" }, "cpython-3.12.11+debug-linux-riscv64-gnu": { @@ -10891,8 +10971,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-riscv64-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "128949afd8c2ead23abf0210faa20cbd46408e66bba1cc8950b4fcdb54cea8fa", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-riscv64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "3537f9e61a46d50bc500310c0056a7b99d08a09b5ba29142c783d226a7712c4e", "variant": "debug" }, "cpython-3.12.11+debug-linux-s390x-gnu": { @@ -10907,8 +10987,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-s390x-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "b025e7f6acad441a17394d0fc24f696717dd9ec93718000379ca376a02d7c4a6", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-s390x-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "859d643152c2b86621a6aa9cfb9d1f50654b402e852ce9cdc864a2f07652c4f7", "variant": "debug" }, "cpython-3.12.11+debug-linux-x86_64-gnu": { @@ -10923,8 +11003,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-x86_64-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "99ab13c789dffdf0b43e0c206fd69c73a6713527bf74c984c7e99bab8362ab45", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-x86_64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "59aaff170ce2a6efe4a34e5ed50db83c4e29951e3c83ac7fa031f4b8247a440e", "variant": "debug" }, "cpython-3.12.11+debug-linux-x86_64-musl": { @@ -10939,8 +11019,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-x86_64-unknown-linux-musl-debug-full.tar.zst", - "sha256": "53e842d33cc5a61ee2e954a431ea100a59fa5ae53e355d5f34c00c1555a810ce", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-x86_64-unknown-linux-musl-debug-full.tar.zst", + "sha256": "9b43f05f37cd39ff45021d8a13b219cb6ad970130c1c32463cd563002aaff9a7", "variant": "debug" }, "cpython-3.12.11+debug-linux-x86_64_v2-gnu": { @@ -10955,8 +11035,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "790b88b4acfc9f58ce1355aba4c73999adf733ac7f8ef87b92b4b092003a0f05", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "37ba61db27c6a1f9d540db6404ea2dfe1f0a221d08332b35476e2d88dd517a0c", "variant": "debug" }, "cpython-3.12.11+debug-linux-x86_64_v2-musl": { @@ -10971,8 +11051,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-x86_64_v2-unknown-linux-musl-debug-full.tar.zst", - "sha256": "88e92e303d1f61f232f15fe3b266abf49981430d4082451dfda3b0c16900f394", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-x86_64_v2-unknown-linux-musl-debug-full.tar.zst", + "sha256": "1c1fc0c7185caf9eaa5914b00176cbf9a2c4bc6922e6dde52587f3125c8f9db4", "variant": "debug" }, "cpython-3.12.11+debug-linux-x86_64_v3-gnu": { @@ -10987,8 +11067,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "701553f500dc2f525ce6a4c78891db14942b1a58fb1b5fa4c4c63120208e9edb", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "2c938382641e5d73c4b148396289f5c047ddbaa9c765ec541983c212e1e02407", "variant": "debug" }, "cpython-3.12.11+debug-linux-x86_64_v3-musl": { @@ -11003,8 +11083,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-x86_64_v3-unknown-linux-musl-debug-full.tar.zst", - "sha256": "8e3c803ed8d3a32800b1d1b5c3279e11aac1ee07bf977288f77d827ab210794f", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-x86_64_v3-unknown-linux-musl-debug-full.tar.zst", + "sha256": "ca3e3795de99768fef314cd40c9b1afe52a4ee6e1409121e8b8d84f524043716", "variant": "debug" }, "cpython-3.12.11+debug-linux-x86_64_v4-gnu": { @@ -11019,8 +11099,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "c66f966c7d32e36851126756ba6ce7cfc6ec0fd2f257deb22420b98fb1790364", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "6c8cfad74f3ff99f275717096401021a15b92939fac1ce2ba0803d78beb8fa45", "variant": "debug" }, "cpython-3.12.11+debug-linux-x86_64_v4-musl": { @@ -11035,8 +11115,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.12.11%2B20250626-x86_64_v4-unknown-linux-musl-debug-full.tar.zst", - "sha256": "438d8cfb97c881636811adf2bceaac28c756cccf852c460716d5f616efae3698", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-x86_64_v4-unknown-linux-musl-debug-full.tar.zst", + "sha256": "f4ec5b942fc2ddf55f26ec6a25b2ddfcda94fe095e158582a8f523258ee09cc4", "variant": "debug" }, "cpython-3.12.10-darwin-aarch64-none": { @@ -15083,8 +15163,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-aarch64-apple-darwin-install_only_stripped.tar.gz", - "sha256": "e484c41e7a5c35b2956ac547c4f16fc2f3b4279b480ba3b89c8aef091aa4b51d", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-aarch64-apple-darwin-install_only_stripped.tar.gz", + "sha256": "89079bf9233e4305fac31fceef3e11f955ab78e3e3b0eedd8dabda39ca21558d", "variant": null }, "cpython-3.11.13-darwin-x86_64-none": { @@ -15099,8 +15179,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-x86_64-apple-darwin-install_only_stripped.tar.gz", - "sha256": "1d18084cfa3347dc5e1a343cfd42d03de7ef69c93bf5ad31b105cfe12d7e502d", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-x86_64-apple-darwin-install_only_stripped.tar.gz", + "sha256": "441c0ef2ed9ee20d762c489dc3f2489d53d5a2b811af675fec2c0786273dd301", "variant": null }, "cpython-3.11.13-linux-aarch64-gnu": { @@ -15115,8 +15195,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "8c26b055937a25ccf17b7578bad42ce6c9fb452a6d4b1d72816755e234922668", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "7d6fb24f7d81af6a89d1a4358cc007ed513747c5beda578fb62a41e139ce0035", "variant": null }, "cpython-3.11.13-linux-armv7-gnueabi": { @@ -15131,8 +15211,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-armv7-unknown-linux-gnueabi-install_only_stripped.tar.gz", - "sha256": "17e6023581689c1bd51f37a381e0700770e387d7696bf8d6c7dae3bcea7fdd61", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-armv7-unknown-linux-gnueabi-install_only_stripped.tar.gz", + "sha256": "cb04fcda5b56cc274893e85f01ce536e5cc540c43352fc47f8b66280ffa1afaa", "variant": null }, "cpython-3.11.13-linux-armv7-gnueabihf": { @@ -15147,8 +15227,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-armv7-unknown-linux-gnueabihf-install_only_stripped.tar.gz", - "sha256": "5a26f2dac227f6667724567027a4b502dea2e27b1105110a71d5a5df0b144a88", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-armv7-unknown-linux-gnueabihf-install_only_stripped.tar.gz", + "sha256": "542cd5b270c16f841993fb63ecdb8ab3d85d6087cfef929304011949f3b6902e", "variant": null }, "cpython-3.11.13-linux-powerpc64le-gnu": { @@ -15163,8 +15243,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-ppc64le-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "8273efc027cce7fb3e5ec2a283be0266db76a75dea79bccf24470ff412835305", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-ppc64le-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "fa361259ac188fb6ee21c6928acfbb0ae287af5c5acbb01c8c55880d72d53d22", "variant": null }, "cpython-3.11.13-linux-riscv64-gnu": { @@ -15179,8 +15259,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-riscv64-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "cf4719c427131c8d716b0642ddfc84d08d0e870f29cc54c230f3db188da44c72", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-riscv64-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "23970dd65b2481eb8c7ddd211ba9841f7eb0927a9a3107a520a75ca97be3df3b", "variant": null }, "cpython-3.11.13-linux-s390x-gnu": { @@ -15195,8 +15275,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-s390x-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "2ec2fd56109668b455effb6201c9ab716d66bd59e1d144fa2ab4f727afa61c19", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-s390x-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "c0d3c3a102df12839c7632fb82e2210ccc0045c7b6cc3fc355e0fda30a9e6745", "variant": null }, "cpython-3.11.13-linux-x86_64-gnu": { @@ -15211,8 +15291,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "c6dd8a6fef05c28710ec62fab10d9343b61bbb9f40d402dad923497d7429fd17", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "07c15c63bdf6d14d80a50ec3ed9a0e81d04b9cf9671decfdec3d508ab252a713", "variant": null }, "cpython-3.11.13-linux-x86_64-musl": { @@ -15227,8 +15307,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-x86_64-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "3d4fcdcd36591d80ca3b0affb6f96d5c5f56c5a344efbd013e406c9346053005", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-x86_64-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "224de8d55c04df9cbf9555b57810dc6f87af82f0902c0883fcf6ed164c99b2cb", "variant": null }, "cpython-3.11.13-linux-x86_64_v2-gnu": { @@ -15243,8 +15323,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-x86_64_v2-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "9cc3c80f518031a0f2af702a38e37d706015bf411144ca29e05756eeee0f32b2", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-x86_64_v2-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "37bc8eb56569dcb033efb69ffcb3a7dcc19220c9b6c88a47e2df1a4bcbc5bce3", "variant": null }, "cpython-3.11.13-linux-x86_64_v2-musl": { @@ -15259,8 +15339,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-x86_64_v2-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "1f1453776244baff8ff7a17d88fd68fdd10c08a950c911dd25cc28845c949421", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-x86_64_v2-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "d64c66b290eff19f4c200f07553f80e6d61afdb3d834b3ac989cda21731f2f59", "variant": null }, "cpython-3.11.13-linux-x86_64_v3-gnu": { @@ -15275,8 +15355,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-x86_64_v3-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "094fef0b6e8d778a61d430a44a784caab0c1be5a2b94d7169b17791c6fdfa2e5", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-x86_64_v3-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "3099ad95267bd3154697079c1c057e84e206bdfc6cdb728f0d47c824f82db260", "variant": null }, "cpython-3.11.13-linux-x86_64_v3-musl": { @@ -15291,8 +15371,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-x86_64_v3-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "526c2c8088c1a41c4bd362c1d463fccaa37129dfe4f28166eabb726664a7550e", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-x86_64_v3-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "231358a2adc7a132c07470577dfffa23353aca7384c1af6e87d0174e859df133", "variant": null }, "cpython-3.11.13-linux-x86_64_v4-gnu": { @@ -15307,8 +15387,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-x86_64_v4-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "27b7bd55029877537242b702badd96846ba1b41346998dfd8c7be191b6b393fa", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-x86_64_v4-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "27c9bec499de6ff14909f030c2d788e41e69d5b5ee2b9deb1f78666f630d6da4", "variant": null }, "cpython-3.11.13-linux-x86_64_v4-musl": { @@ -15323,8 +15403,24 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-x86_64_v4-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "f8dc02227a0156fd28652148486878ef7a50de09a5b8373555a0573cc2347f18", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-x86_64_v4-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "0d7bffc0778796def75b663e31c138592237cd205e358540b4bafd43df109c09", + "variant": null + }, + "cpython-3.11.13-windows-aarch64-none": { + "name": "cpython", + "arch": { + "family": "aarch64", + "variant": null + }, + "os": "windows", + "libc": "none", + "major": 3, + "minor": 11, + "patch": 13, + "prerelease": "", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-aarch64-pc-windows-msvc-install_only_stripped.tar.gz", + "sha256": "e5d8f83e8728506ad08382c7f68328b4204dd52c3f9cb4337087b1f21b68ee2a", "variant": null }, "cpython-3.11.13-windows-i686-none": { @@ -15339,8 +15435,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-i686-pc-windows-msvc-install_only_stripped.tar.gz", - "sha256": "0d1eb6a83db554379555279da7a00ef421306b35f5fd2603087a960c77aef5dc", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-i686-pc-windows-msvc-install_only_stripped.tar.gz", + "sha256": "228e47684fe989db07085f32b8dca766027ba34672252e9140f5bd367e0ba23f", "variant": null }, "cpython-3.11.13-windows-x86_64-none": { @@ -15355,8 +15451,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-x86_64-pc-windows-msvc-install_only_stripped.tar.gz", - "sha256": "709f7f3a0913226230f50c132ab6a1a517206c9d0b2e30c1779574913f0ac74b", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-x86_64-pc-windows-msvc-install_only_stripped.tar.gz", + "sha256": "abde9c8b8b1230f1a21b52efd2e41a43b5af0a754255d294e180238bf6d291e0", "variant": null }, "cpython-3.11.13+debug-linux-aarch64-gnu": { @@ -15371,8 +15467,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-aarch64-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "09a257ac990d66b64f383767154ab8dde452457fd973e403c3ffe73ea2ef6041", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-aarch64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "c07c910236c121818decbdfd947f5033741afc8610f347256179cbda9cee0ccf", "variant": "debug" }, "cpython-3.11.13+debug-linux-armv7-gnueabi": { @@ -15387,8 +15483,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-armv7-unknown-linux-gnueabi-debug-full.tar.zst", - "sha256": "d8dc85bbec1c157e078c66def67ad65157a96ba19fcde8962131a41f4f600e98", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-armv7-unknown-linux-gnueabi-debug-full.tar.zst", + "sha256": "2b15db81177c7975393cbae2a5b7989b59318084c0e8ae3798cb5982606bf5d1", "variant": "debug" }, "cpython-3.11.13+debug-linux-armv7-gnueabihf": { @@ -15403,8 +15499,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-armv7-unknown-linux-gnueabihf-debug-full.tar.zst", - "sha256": "b27b622310194321853d106f4b344b73592b5ca85b1cc9ed3bdb19bdb3d6f0d0", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-armv7-unknown-linux-gnueabihf-debug-full.tar.zst", + "sha256": "b3f948f00201905cf6a7448030a40a51213b3f937f29f501114354f4cc419886", "variant": "debug" }, "cpython-3.11.13+debug-linux-powerpc64le-gnu": { @@ -15419,8 +15515,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-ppc64le-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "8ba75ecfead212f9f5e9b8e2cbc2ad608f5b6812619da4414fd6b283f2acbf78", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-ppc64le-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "7f2f6ceb4f242e71977eefd5e0e702c5e87d6600342d79e6dadaf2e59ed5476f", "variant": "debug" }, "cpython-3.11.13+debug-linux-riscv64-gnu": { @@ -15435,8 +15531,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-riscv64-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "c199f80c7d4502ba3d710c4c7450f81642bdac5524079829b7c7b8002b9747a8", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-riscv64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "7454ccaae24cf8742ea55ac74b0b1d147be3f99bf290c7c5a02f0027a36805ac", "variant": "debug" }, "cpython-3.11.13+debug-linux-s390x-gnu": { @@ -15451,8 +15547,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-s390x-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "23934c0dc86aeb1c5d0f0877ac8d9d6632627a0b60c9f4f9ad9db41278b6745f", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-s390x-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "9fa9c461b14dd142e15cf95db1b0ca7310ea603fec250eb1952a8d31b64027f0", "variant": "debug" }, "cpython-3.11.13+debug-linux-x86_64-gnu": { @@ -15467,8 +15563,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-x86_64-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "a405a9b2bc94b564a03f351b549c708f686aeade9aec480108b613332cf9cc48", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-x86_64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "533d8ccdb77fc43847227850361809c9bfb85223a08d689e31087df34d8260ac", "variant": "debug" }, "cpython-3.11.13+debug-linux-x86_64-musl": { @@ -15483,8 +15579,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-x86_64-unknown-linux-musl-debug-full.tar.zst", - "sha256": "2b3fecb1717cf632e789c4b8c03eda825f9c3e112cac922d58b42e7eecb8731f", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-x86_64-unknown-linux-musl-debug-full.tar.zst", + "sha256": "0f4dfe78c30d56acb5e6df28de4058177ac188ddd6ea1f2d3a18db7fcfe7ff1b", "variant": "debug" }, "cpython-3.11.13+debug-linux-x86_64_v2-gnu": { @@ -15499,8 +15595,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "ab59a4df1e7d269400116e87daaa25b800ffbb24512b57f8d2aa4adbe3d43577", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "03a7b763a2e5deec621813a9081858aad3ed0f24368ff94bf4a78eb652ffa8a0", "variant": "debug" }, "cpython-3.11.13+debug-linux-x86_64_v2-musl": { @@ -15515,8 +15611,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-x86_64_v2-unknown-linux-musl-debug-full.tar.zst", - "sha256": "1fc167ea607ef98b6d0dbac121a9ae8739fdf958f21cbbd60cac47b7ce52b003", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-x86_64_v2-unknown-linux-musl-debug-full.tar.zst", + "sha256": "b5e1e4c7fc8a3eff8a85f4b4dd004f7d835a8ac79a982da65054d605f106c5eb", "variant": "debug" }, "cpython-3.11.13+debug-linux-x86_64_v3-gnu": { @@ -15531,8 +15627,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "6140e14e713077169826458a47a7439938b98a73810c5e7a709c9c20161ae186", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "d020b9ad0fb3afc95ccf6c9737092f8ea4e977d8f6c0bd2503cd60457113fbfa", "variant": "debug" }, "cpython-3.11.13+debug-linux-x86_64_v3-musl": { @@ -15547,8 +15643,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-x86_64_v3-unknown-linux-musl-debug-full.tar.zst", - "sha256": "77cf2b966c521c1036eb599ab19e3f6e0d987dbb9ba5daa86d0b59d7d1a609a1", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-x86_64_v3-unknown-linux-musl-debug-full.tar.zst", + "sha256": "2f04db73b5fb332f37e409ec048e09a60fd1da729893f629ae37c4991f354d0e", "variant": "debug" }, "cpython-3.11.13+debug-linux-x86_64_v4-gnu": { @@ -15563,8 +15659,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "92583c28fdcbbf716329a6711783e2fb031bb8777e8a99bd0b4d3eed03ec0551", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "8a5ea7c66a05947522d528cb47e03dd795ab108e78b0926039119e9f8341e5b0", "variant": "debug" }, "cpython-3.11.13+debug-linux-x86_64_v4-musl": { @@ -15579,8 +15675,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.11.13%2B20250626-x86_64_v4-unknown-linux-musl-debug-full.tar.zst", - "sha256": "dda82882159f45a926486bc221739c2ff5c0b1d9fa705a51d0f3f729f736162c", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-x86_64_v4-unknown-linux-musl-debug-full.tar.zst", + "sha256": "9829333c2b776ff11ab2e4be31237dc6c36cb2e13c04c361c7c57f4d54c6af3b", "variant": "debug" }, "cpython-3.11.12-darwin-aarch64-none": { @@ -19371,8 +19467,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-aarch64-apple-darwin-install_only_stripped.tar.gz", - "sha256": "e610b8bb397513908b814cb2a1c9f95e97aac33855840bf69f1442ff040ddd33", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-aarch64-apple-darwin-install_only_stripped.tar.gz", + "sha256": "c34bfae5fe85a28c1b13c2d160268eafee18b66f25c29b139958e164897a2527", "variant": null }, "cpython-3.10.18-darwin-x86_64-none": { @@ -19387,8 +19483,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-x86_64-apple-darwin-install_only_stripped.tar.gz", - "sha256": "5c4d02b1626fd677ee3bc432d77e6dca5bbb9b1f03b258edd4c8cf6902eba902", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-x86_64-apple-darwin-install_only_stripped.tar.gz", + "sha256": "f2676d7b52e94334c4564c441c0aeabda9858f29384cbaf824c8a091b574f938", "variant": null }, "cpython-3.10.18-linux-aarch64-gnu": { @@ -19403,8 +19499,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "12519960127a5082410496f59928c3ed1c2d37076d6400b22e52ee962e315522", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "b6f3243caa1fdcf12d62e1711e8a8255ae419632e1e98b6b1fa9809564af82f0", "variant": null }, "cpython-3.10.18-linux-armv7-gnueabi": { @@ -19419,8 +19515,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-armv7-unknown-linux-gnueabi-install_only_stripped.tar.gz", - "sha256": "6dd0d28f7229d044ec1560e11dcbb5452166921c4a931aeae9b9f08f61451eb9", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-armv7-unknown-linux-gnueabi-install_only_stripped.tar.gz", + "sha256": "c091409b05c986623be140e87bedea2a7f2d005dbf1564b9b529d3b3727f5608", "variant": null }, "cpython-3.10.18-linux-armv7-gnueabihf": { @@ -19435,8 +19531,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-armv7-unknown-linux-gnueabihf-install_only_stripped.tar.gz", - "sha256": "810089263699a427f8610070ba7ebad2a78dac894e93a6c055ec28f3303c704e", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-armv7-unknown-linux-gnueabihf-install_only_stripped.tar.gz", + "sha256": "326c378407486a185c516d927652978728f056af6d62150c70678f6b590c1df9", "variant": null }, "cpython-3.10.18-linux-powerpc64le-gnu": { @@ -19451,8 +19547,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-ppc64le-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "263befb4049ed1a272b9299ce41f5d5ef855998462a620dd6b62ecfde47d5154", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-ppc64le-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "3ec27bd1f7f428fb12d9973d84fe268eca476097ab3ab650b4b73fd21861832a", "variant": null }, "cpython-3.10.18-linux-riscv64-gnu": { @@ -19467,8 +19563,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-riscv64-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "0999d7be0eb75b096791f2f57369dd1a6f4cd9dc44eb9720886268e3a3adddd7", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-riscv64-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "9da0a695abfd3a18355642055f79bc0d227e05c03d0430c308e02143fc7dbf9d", "variant": null }, "cpython-3.10.18-linux-s390x-gnu": { @@ -19483,8 +19579,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-s390x-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "2ac9a0791f893b96c4a48942aa2f19b16ddbf60977db63de513beef279599760", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-s390x-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "8192b48982c0f0c0ff642bf84bcf26003812eaac5ef4853ba9b7b6e80745923a", "variant": null }, "cpython-3.10.18-linux-x86_64-gnu": { @@ -19499,8 +19595,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "9d1a87e52e2be1590a6a5148f1b874ba4155095c11e5afad7cb9618e7a4a1912", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "11b27504b26ea1977bf7b7e6ef6c9da4d648b78707fa34fe0538f08744978a4b", "variant": null }, "cpython-3.10.18-linux-x86_64-musl": { @@ -19515,8 +19611,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-x86_64-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "c85cac7c12bb3c6bf09801f9b3f95d77acb5aa5de10a85aeceafb026d641c62c", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-x86_64-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "f5302f08043468f9c9a47fa7884393c27de66b19b3e46096984245d8f4d8be8f", "variant": null }, "cpython-3.10.18-linux-x86_64_v2-gnu": { @@ -19531,8 +19627,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-x86_64_v2-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "d5f70ab75fc24ab3efa4b2edb14163bb145c1f9d297d03dde6c2a40ccb0eb9ac", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-x86_64_v2-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "e979cf0596697fc729d42c5d6d37e1d591374ac56c42d1b5db9e05750984f122", "variant": null }, "cpython-3.10.18-linux-x86_64_v2-musl": { @@ -19547,8 +19643,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-x86_64_v2-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "7800a067ffb6ebd9c226c075da8c57ff843f9b9e7b89b9c14e013bc33f042e4e", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-x86_64_v2-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "d9d0c7e28bc0f5f2a8ce0a9f4d57f8fe263361f5fd7d8afd6f8eeecc7ce00f96", "variant": null }, "cpython-3.10.18-linux-x86_64_v3-gnu": { @@ -19563,8 +19659,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-x86_64_v3-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "ab36f3e99a1fb89cb225fe20a585068a5b4323d084b4441ce0034177b8d8c3bf", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-x86_64_v3-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "3438a6066eb9f801f5a3153d7ee5fb0d1bf7769e44a610effd2340e846a9fd44", "variant": null }, "cpython-3.10.18-linux-x86_64_v3-musl": { @@ -19579,8 +19675,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-x86_64_v3-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "7e91185f8d1e614c85447561752476b7c079e63df9a707bb9b4c0f1649446e00", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-x86_64_v3-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "ae4eeba3037b6810eb9e96814d3980e28a5a3de5277f470100742a48391c6df7", "variant": null }, "cpython-3.10.18-linux-x86_64_v4-gnu": { @@ -19595,8 +19691,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-x86_64_v4-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "65cfa877bc7899f0a458ff5327311e77e0b70fa1c871aeb6dfa582d23422337e", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-x86_64_v4-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "c8cde20b14e0ef0e564d5a1cbd0c6003ae178dfea9d446cbf3ee15d75ac8a9d8", "variant": null }, "cpython-3.10.18-linux-x86_64_v4-musl": { @@ -19611,8 +19707,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-x86_64_v4-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "869b5a0919558fe8bbdac4d75a9e9a114a4aa7ca0905da4243ec1b7e4ff99006", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-x86_64_v4-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "46da91101e88f68f5fa21a2aeadeeb91072cbe9d9f1caa08e34481180ec3dea3", "variant": null }, "cpython-3.10.18-windows-i686-none": { @@ -19627,8 +19723,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-i686-pc-windows-msvc-install_only_stripped.tar.gz", - "sha256": "a0a5edc7cff851a59dca8471858262a2bb3708b00ad443dd908c3b67384b8ee4", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-i686-pc-windows-msvc-install_only_stripped.tar.gz", + "sha256": "faab88604275a0c869cf3fb19f63b0158bd8356d74fff15ebd05114908683fb1", "variant": null }, "cpython-3.10.18-windows-x86_64-none": { @@ -19643,8 +19739,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-x86_64-pc-windows-msvc-install_only_stripped.tar.gz", - "sha256": "d6e97e2984ff157d3b5caf9078eb45613684a39368da2fbac8dd364080322e82", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-x86_64-pc-windows-msvc-install_only_stripped.tar.gz", + "sha256": "ac56903c7ae726f154b5b000f04db72ddb52775812c638cc67b5090146267502", "variant": null }, "cpython-3.10.18+debug-linux-aarch64-gnu": { @@ -19659,8 +19755,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-aarch64-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "e42a95657c84804f0edf02db89e12e907e1fb9d8c92b707214be7c1b3dc0f4d5", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-aarch64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "fcbbf8d8e274f7983a70e104bf708ff21effc28bb594314429317e73960f85ff", "variant": "debug" }, "cpython-3.10.18+debug-linux-armv7-gnueabi": { @@ -19675,8 +19771,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-armv7-unknown-linux-gnueabi-debug-full.tar.zst", - "sha256": "e733a7aa76137d5a795ec5db08d5c37d06e111e2343d70308f09789ced7b4d32", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-armv7-unknown-linux-gnueabi-debug-full.tar.zst", + "sha256": "400ab706d0f21b68793379040f9fa96fce9cacfff25217aaa3f37cac8d15fea5", "variant": "debug" }, "cpython-3.10.18+debug-linux-armv7-gnueabihf": { @@ -19691,8 +19787,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-armv7-unknown-linux-gnueabihf-debug-full.tar.zst", - "sha256": "06ec2977ec2d01f3225272926ab43d66f976f1481d069d9a618a0541b2644451", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-armv7-unknown-linux-gnueabihf-debug-full.tar.zst", + "sha256": "48a8a9cbf0b6a9bfd95af64af97dda167d99240cd3f66265011006f2d2a54e34", "variant": "debug" }, "cpython-3.10.18+debug-linux-powerpc64le-gnu": { @@ -19707,8 +19803,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-ppc64le-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "2ae4ed1805b647f105db8c41d9ac55fcda2672526b63c2e1aa9d0eb16a537594", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-ppc64le-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "8571d2b81798162c1effb57370359fe178d96f3a5a6bb15f5bd88c83caf8c6b4", "variant": "debug" }, "cpython-3.10.18+debug-linux-riscv64-gnu": { @@ -19723,8 +19819,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-riscv64-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "c9fdef27a8295c29c4ba6fd2daff344c12471f97ca7a19280c3e0f7955438668", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-riscv64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "dedf8c8bdae3b3d4d8ec028c28bb8ad12f9c78064828426f6570322eba490015", "variant": "debug" }, "cpython-3.10.18+debug-linux-s390x-gnu": { @@ -19739,8 +19835,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-s390x-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "eac2aab5eaea5e8df9c24918c0ade7a003abe6d25390d4a85e7dd8eea6eee1e3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-s390x-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "ffd41140c4f55b9700be4381a0ef774067486572156ec17839e38f360512cc3e", "variant": "debug" }, "cpython-3.10.18+debug-linux-x86_64-gnu": { @@ -19755,8 +19851,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-x86_64-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "1d651aebc511e100a80142b78d2ea4655a6309f5a87a6cbd557fed5afda28f33", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-x86_64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "ec0b1c02cb0813db9d9bfb1c41633c15c73446d74406d07df0de041dadcd2d7b", "variant": "debug" }, "cpython-3.10.18+debug-linux-x86_64-musl": { @@ -19771,8 +19867,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-x86_64-unknown-linux-musl-debug-full.tar.zst", - "sha256": "c84b94373b1704d5d3a2f4c997c3462d1a03ebbd141eeeaec58da411e1cd62c1", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-x86_64-unknown-linux-musl-debug-full.tar.zst", + "sha256": "448962b7ac159470130fa32817f917c5db659d5adde6c300c93335fdb0915d92", "variant": "debug" }, "cpython-3.10.18+debug-linux-x86_64_v2-gnu": { @@ -19787,8 +19883,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "72eacef9b8752ad823dbc009aa9be2b3199f7556218a63a4de0f1cb1b6bd08ec", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "a6d0b714ee3b0b0aae36dbb6c5a28378717c99070797ffda12ddd433a8f8588c", "variant": "debug" }, "cpython-3.10.18+debug-linux-x86_64_v2-musl": { @@ -19803,8 +19899,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-x86_64_v2-unknown-linux-musl-debug-full.tar.zst", - "sha256": "01a9ddcf418a74805c17ceaa9ecdcbe0871f309e0649b43e6cd6f0fe883d8a14", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-x86_64_v2-unknown-linux-musl-debug-full.tar.zst", + "sha256": "09903f2c08fccddce4f7a5d52c2956d898a70a7e47bb07825581e77ad9a77829", "variant": "debug" }, "cpython-3.10.18+debug-linux-x86_64_v3-gnu": { @@ -19819,8 +19915,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "06d979ecd8256d978f5a76f90063b198f08bb6833ead81e52a32f0213337f333", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "c453e8adeed1f0dc8253bbd6ebd72d9803b88fbfd015049b3d281ce915b0dfbf", "variant": "debug" }, "cpython-3.10.18+debug-linux-x86_64_v3-musl": { @@ -19835,8 +19931,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-x86_64_v3-unknown-linux-musl-debug-full.tar.zst", - "sha256": "f910de7dcd093e74d45b349bcd7a0cf291602979a90ef682fcf2b90a6bd4e301", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-x86_64_v3-unknown-linux-musl-debug-full.tar.zst", + "sha256": "054fdb4b6d27155371cdfa9188a93c4d58325ebb9a061ded8ad7b044cc177064", "variant": "debug" }, "cpython-3.10.18+debug-linux-x86_64_v4-gnu": { @@ -19851,8 +19947,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "c3516587af46e3e38115b282f8585960e5050ad9a822e8d85c4159f1a209960f", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "0e47c951e3800a10f531ef4eb4d3307822f76674dbe863c11a48547ae51f0e27", "variant": "debug" }, "cpython-3.10.18+debug-linux-x86_64_v4-musl": { @@ -19867,8 +19963,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.10.18%2B20250626-x86_64_v4-unknown-linux-musl-debug-full.tar.zst", - "sha256": "827cbcaad8cb5e16192f2f89f2e62a48ee3be89ec924d83e526f213806433d4a", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-x86_64_v4-unknown-linux-musl-debug-full.tar.zst", + "sha256": "7f625673e2d06abac133fd9588b6492c3b6b69b6cacd1b1bb480b9476316ab74", "variant": "debug" }, "cpython-3.10.17-darwin-aarch64-none": { @@ -24811,8 +24907,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-aarch64-apple-darwin-install_only_stripped.tar.gz", - "sha256": "dbb161ced093b9b3b7f93ae7064fe20821471c2a4ac884e2bc94a216a2e19cba", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-aarch64-apple-darwin-install_only_stripped.tar.gz", + "sha256": "f47a96f6bcf29ef0e803c8524b027c4c1b411d27934e6521ebe3381f9d674f7b", "variant": null }, "cpython-3.9.23-darwin-x86_64-none": { @@ -24827,8 +24923,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-x86_64-apple-darwin-install_only_stripped.tar.gz", - "sha256": "b7dbbec3f74512ce7282ffeb9d262c3605e824f564c49c698a83d8ad9a9810df", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-x86_64-apple-darwin-install_only_stripped.tar.gz", + "sha256": "04fe6e2fd53c9dc4dfbcf870174c44136f82690f7387451bf8863712314eb156", "variant": null }, "cpython-3.9.23-linux-aarch64-gnu": { @@ -24843,8 +24939,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "f8886fd05163ff181bc162399815e2c74b2dc85831a05ce76472fe10a0e509d6", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "324e4aaba0d4749d9d9a2007a7fb2e55c6c206241c8607ade29e9a61a70f23c0", "variant": null }, "cpython-3.9.23-linux-armv7-gnueabi": { @@ -24859,8 +24955,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-armv7-unknown-linux-gnueabi-install_only_stripped.tar.gz", - "sha256": "f05ac5a425eaf4ae61bfb8981627fb6b6a6224733d9bfbe79f1c9cd89694fa1a", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-armv7-unknown-linux-gnueabi-install_only_stripped.tar.gz", + "sha256": "5f88c66314c7820cb20ca3fef3c191850d0247c78d66cb366590ebb813b36b4d", "variant": null }, "cpython-3.9.23-linux-armv7-gnueabihf": { @@ -24875,8 +24971,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-armv7-unknown-linux-gnueabihf-install_only_stripped.tar.gz", - "sha256": "c5cb4da17943d74c1a94b6894d9960d96e8e4afc83e629a5788adbd2f8f4d51f", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-armv7-unknown-linux-gnueabihf-install_only_stripped.tar.gz", + "sha256": "2a4a348c42b424f5d01ff61882edde283e0135331b54349f5bc41f70282fc56f", "variant": null }, "cpython-3.9.23-linux-powerpc64le-gnu": { @@ -24891,8 +24987,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-ppc64le-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "815e88f67598ebb039e1735a5d5f4f9facf61481111657a3ecefb69993a6a8ab", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-ppc64le-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "f80b1a541b74122260372e36504157a94b7b036626779b215f66e26174c16ba1", "variant": null }, "cpython-3.9.23-linux-riscv64-gnu": { @@ -24907,8 +25003,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-riscv64-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "595c1ca3dbad46cc2370e17a5d415da82b75140a057449fc7de5d41faa2cc87a", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-riscv64-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "4a57ad0c9a214e97cf18ed366f858b224e7a4c3da8519adcb0f757fb4939c64e", "variant": null }, "cpython-3.9.23-linux-s390x-gnu": { @@ -24923,8 +25019,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-s390x-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "e6b2c9ca1962a069959f70d304e15ba0e63506db6e1b6bc3c75d03ca7012ac9f", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-s390x-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "f76716832859664fb3bb89708fd60e55cf7bbb9fd4066a1746dda26c9baa633a", "variant": null }, "cpython-3.9.23-linux-x86_64-gnu": { @@ -24939,8 +25035,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "e6707a3a40fc68f37bad9b7ad9e018715af3be057b62bc80fee830a161c775f3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "a4c3137284a68bcae77e794873978703b079ed3355c86f2d9e3e1e71b2910ccb", "variant": null }, "cpython-3.9.23-linux-x86_64-musl": { @@ -24955,8 +25051,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-x86_64-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "9ce6c722a5436f4adc1b25c01c5ada2c230f0089302a544d35b5b80702c0a7db", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-x86_64-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "a501bfae7b0c8f3fbfd4de76698face42af7f878395b22a7d567c9117f118c43", "variant": null }, "cpython-3.9.23-linux-x86_64_v2-gnu": { @@ -24971,8 +25067,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-x86_64_v2-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "d6d6329699af5d463cfecef304e739347b1e485d959a63dc54b39a818dd0c5dd", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-x86_64_v2-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "5a609ff46904d28acbc52cfa6b445066ec98ca690ffa8870fdd17537b72d98b0", "variant": null }, "cpython-3.9.23-linux-x86_64_v2-musl": { @@ -24987,8 +25083,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-x86_64_v2-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "eb082839a9dd585e151395041f7832bb725a3bfc4e153e3f41949d43163902ab", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-x86_64_v2-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "a041d91af09e4246f463361ec76de06080fd1cf05771f59e13a1fbbc42fb3d6f", "variant": null }, "cpython-3.9.23-linux-x86_64_v3-gnu": { @@ -25003,8 +25099,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-x86_64_v3-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "2a0080b853d55ae9db6b20209d63fbd4cacc794be647e7f9cf1a342dfd7e5796", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-x86_64_v3-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "b7bd686ea5e00697498baead5a01abe0ceb4a0c9e6fbed5823b0c8225fb25176", "variant": null }, "cpython-3.9.23-linux-x86_64_v3-musl": { @@ -25019,8 +25115,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-x86_64_v3-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "3eb47ce30946e30415538acf0e7a3efbac4679d5d16900c5c379f77ebef3321d", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-x86_64_v3-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "4daf37cac1f2fcc6696c64f0a9705f01121046f9b4a3c5424b83714a61161a5b", "variant": null }, "cpython-3.9.23-linux-x86_64_v4-gnu": { @@ -25035,8 +25131,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-x86_64_v4-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "2a81c218277a16515531df2568b8dc72cd1b2a2c22268882d57881a6f0f931d4", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-x86_64_v4-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "4cf404087df0b1deb8332725583789078b868b5baa6aa5375e168d3cdb987e7f", "variant": null }, "cpython-3.9.23-linux-x86_64_v4-musl": { @@ -25051,8 +25147,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-x86_64_v4-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "a30191e1ecd58075909c5f28f21ddc290687afaa216b5cdd8c1f5a97963d1a95", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-x86_64_v4-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "f7e827cc26ce8051e77ec7a7bea2d658a1f924664e0725c21a67ab15d6f77bf8", "variant": null }, "cpython-3.9.23-windows-i686-none": { @@ -25067,8 +25163,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-i686-pc-windows-msvc-install_only_stripped.tar.gz", - "sha256": "f0482615c924623c3441ea96bfa1ea748b597db9099b8ad2e7c51526d799020b", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-i686-pc-windows-msvc-install_only_stripped.tar.gz", + "sha256": "09e4db6bd06c7b26e437df662fda4d507f276ee1ba90b986e5596a823f5d980b", "variant": null }, "cpython-3.9.23-windows-x86_64-none": { @@ -25083,8 +25179,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-x86_64-pc-windows-msvc-install_only_stripped.tar.gz", - "sha256": "d73d4aee5f2097cd1f5cc16a0f345c7cb53c3e8b94f1ca007b918a3152185c4b", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-x86_64-pc-windows-msvc-install_only_stripped.tar.gz", + "sha256": "3c50ad97c45bcf0b2984479ebe896f61032222605d317ad059a481730a0ee72a", "variant": null }, "cpython-3.9.23+debug-linux-aarch64-gnu": { @@ -25099,8 +25195,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-aarch64-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "140645dbd801afa90101806401cb2f855cd243e7b88a6c3bce665e39c252c6e1", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-aarch64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "751f68beb0dd5067bde56d9a4028b0233e2a71f84bf421e8627dc2abd068d954", "variant": "debug" }, "cpython-3.9.23+debug-linux-armv7-gnueabi": { @@ -25115,8 +25211,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-armv7-unknown-linux-gnueabi-debug-full.tar.zst", - "sha256": "b6e8223142843640e8e5acbf96eaea72f2e7218e9da61de1d2215a626ebe207b", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-armv7-unknown-linux-gnueabi-debug-full.tar.zst", + "sha256": "29ee738ac4d186bb431c4382a6d1cc550a0916a1ce99d58769fc5371faff8409", "variant": "debug" }, "cpython-3.9.23+debug-linux-armv7-gnueabihf": { @@ -25131,8 +25227,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-armv7-unknown-linux-gnueabihf-debug-full.tar.zst", - "sha256": "1b9cc881bcf52e9dd22da45782a11beda7de63e5d0644092412542ec8c3c2ce8", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-armv7-unknown-linux-gnueabihf-debug-full.tar.zst", + "sha256": "afea4baf253f4bf91781c122308ca306d72fa1e76526abf2b78648772c979d2c", "variant": "debug" }, "cpython-3.9.23+debug-linux-powerpc64le-gnu": { @@ -25147,8 +25243,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-ppc64le-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "93cdb8c7bc5a13c273855b97e827a3b9b12522b7d7ed14e5110a7fa5043f7654", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-ppc64le-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "0c29b50f57489b93b29a6fe00cb24fb84cd6713c065e35794d1d29d81485c01d", "variant": "debug" }, "cpython-3.9.23+debug-linux-riscv64-gnu": { @@ -25163,8 +25259,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-riscv64-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "fb04b810e7980a211ab926f0db3430919790b86dee304682e2453669063fff34", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-riscv64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "e4c17ce1f909fe47049fb3236301dde356d4ccc456f70e59e31df8f6b6e21f7d", "variant": "debug" }, "cpython-3.9.23+debug-linux-s390x-gnu": { @@ -25179,8 +25275,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-s390x-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "394cd53c4c012d7c32950600462a68fe0ed52f5204f745a7ebbc19f2473121b3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-s390x-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "8f2783d56728a918568f8ec81d2b7c8b0e4060e605f42b4ae50b9f127925b54c", "variant": "debug" }, "cpython-3.9.23+debug-linux-x86_64-gnu": { @@ -25195,8 +25291,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-x86_64-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "a772240194dd997da21e53029fde922da7e1893af1356eb47d710b2fbf144b0e", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-x86_64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "f4ef26643dfb13a80439373ce4e192f9c678ccffce8bda9455258052c29e0c85", "variant": "debug" }, "cpython-3.9.23+debug-linux-x86_64-musl": { @@ -25211,8 +25307,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-x86_64-unknown-linux-musl-debug-full.tar.zst", - "sha256": "8153da9228087fc1ed655a83eb304318cc646333966ccb9ccd75ab9589b8585f", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-x86_64-unknown-linux-musl-debug-full.tar.zst", + "sha256": "f74953fe519fbdbb35ce722c97521b89c915da7d630af5a82911dd352f5c8cec", "variant": "debug" }, "cpython-3.9.23+debug-linux-x86_64_v2-gnu": { @@ -25227,8 +25323,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "79d6c604c491c02cff2e1ffd632baf4d2418a85fe10dd1260ec860dde92322c1", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "039ada74a3fea0f359a617a57e83ee92c43d5770175bb3c26d96ac65364f7f5c", "variant": "debug" }, "cpython-3.9.23+debug-linux-x86_64_v2-musl": { @@ -25243,8 +25339,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-x86_64_v2-unknown-linux-musl-debug-full.tar.zst", - "sha256": "7e222a1a6333e1e10bf476ac37ca2734885cb2cf92a7a8d3dc4f7fb81f69683b", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-x86_64_v2-unknown-linux-musl-debug-full.tar.zst", + "sha256": "64e72ce5b13bff5f7f9e44032ecf6ff70212b7384d2512ab5ed8b6edddd90e68", "variant": "debug" }, "cpython-3.9.23+debug-linux-x86_64_v3-gnu": { @@ -25259,8 +25355,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "a03b38980fb6578be1a7c8e78f7219662e72ac1dc095b246d5a0b06d27e61ece", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "9a525d2a45db88cf7b450b2c0226204c1e5f634e3fb793f345ff6d44a9bac0fb", "variant": "debug" }, "cpython-3.9.23+debug-linux-x86_64_v3-musl": { @@ -25275,8 +25371,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-x86_64_v3-unknown-linux-musl-debug-full.tar.zst", - "sha256": "93df57f432ad2741e8edeefb1caf7a3d538f8c90cac732c6865d914d17577aed", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-x86_64_v3-unknown-linux-musl-debug-full.tar.zst", + "sha256": "7370898765b907757b7220db752c91de9a1832460d665845c74c4c946bbdd4ec", "variant": "debug" }, "cpython-3.9.23+debug-linux-x86_64_v4-gnu": { @@ -25291,8 +25387,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "2ba7ac2755c52728882cf187e8f2127b0b89cade6eaa2d42dd379d1bcd01a0a9", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "c104d6d5d6943ecf53b9cc8560a32239a62c030a1170d1d4c686e87d7da60d58", "variant": "debug" }, "cpython-3.9.23+debug-linux-x86_64_v4-musl": { @@ -25307,8 +25403,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250626/cpython-3.9.23%2B20250626-x86_64_v4-unknown-linux-musl-debug-full.tar.zst", - "sha256": "5253927e725888dae7a39caca1a88dcbfa002b2a115cb6e06000db04e133b51d", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-x86_64_v4-unknown-linux-musl-debug-full.tar.zst", + "sha256": "933c85357368d1819a32bed32cdb871153ecc2a39b93565ae61e5fa0b509e2eb", "variant": "debug" }, "cpython-3.9.22-darwin-aarch64-none": { From 9e9505df50c821c848f5677e58536563493aa0ac Mon Sep 17 00:00:00 2001 From: Adrien Cacciaguerra Date: Tue, 1 Jul 2025 00:58:29 +0200 Subject: [PATCH 139/185] Bump CodSpeed to v3 (#14371) ## Summary As explained in the [`codspeed-rust` v3 release notes](https://github.com/CodSpeedHQ/codspeed-rust/releases/tag/v3.0.0), the `v3` of the compatibility layers is now required to work with the latest version(`v3`) of `cargo-codspeed`. --- Cargo.lock | 186 ++++++++++++++++++++++++++++++++++--- crates/uv-bench/Cargo.toml | 6 +- 2 files changed, 175 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eb2faac26..e317fd224 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -94,6 +94,15 @@ version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + [[package]] name = "arbitrary" version = "1.4.1" @@ -364,6 +373,15 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bisection" version = "0.1.0" @@ -672,22 +690,27 @@ checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "codspeed" -version = "2.10.1" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93f4cce9c27c49c4f101fffeebb1826f41a9df2e7498b7cd4d95c0658b796c6c" +checksum = "38b41c7ae78309311a5ce5f31dbdae2d7d6ad68b057163eaa23e05b68c9ffac8" dependencies = [ + "anyhow", + "bincode", "colored", + "glob", "libc", + "nix 0.29.0", "serde", "serde_json", + "statrs", "uuid", ] [[package]] name = "codspeed-criterion-compat" -version = "2.10.1" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3c23d880a28a2aab52d38ca8481dd7a3187157d0a952196b6db1db3c8499725" +checksum = "b2c699a447da1c442a81e14f01363f44ffd68eb13fedfd3ed13ce5cf30f738d9" dependencies = [ "codspeed", "codspeed-criterion-compat-walltime", @@ -696,9 +719,9 @@ dependencies = [ [[package]] name = "codspeed-criterion-compat-walltime" -version = "2.10.1" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b0a2f7365e347f4f22a67e9ea689bf7bc89900a354e22e26cf8a531a42c8fbb" +checksum = "c104831cdfa515c938d8ec5b8dd38030a5f815b113762f0d03f494177b261cfd" dependencies = [ "anes", "cast", @@ -738,7 +761,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" dependencies = [ "lazy_static", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1115,7 +1138,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1930,7 +1953,7 @@ checksum = "e19b23d53f35ce9f56aebc7d1bb4e6ac1e9c0db7ac85c8d1760c04379edced37" dependencies = [ "hermit-abi 0.4.0", "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1990,7 +2013,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2084,6 +2107,12 @@ version = "0.2.171" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + [[package]] name = "libmimalloc-sys" version = "0.1.39" @@ -2204,6 +2233,16 @@ dependencies = [ "regex-automata 0.1.10", ] +[[package]] +name = "matrixmultiply" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08" +dependencies = [ + "autocfg", + "rawpointer", +] + [[package]] name = "md-5" version = "0.10.6" @@ -2341,6 +2380,23 @@ dependencies = [ "syn", ] +[[package]] +name = "nalgebra" +version = "0.33.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26aecdf64b707efd1310e3544d709c5c0ac61c13756046aaaba41be5c4f66a3b" +dependencies = [ + "approx", + "matrixmultiply", + "num-complex", + "num-rational", + "num-traits", + "rand", + "rand_distr", + "simba", + "typenum", +] + [[package]] name = "nanoid" version = "0.4.0" @@ -2399,6 +2455,45 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -2406,6 +2501,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -2849,7 +2945,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2906,6 +3002,22 @@ dependencies = [ "getrandom 0.2.15", ] +[[package]] +name = "rand_distr" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" +dependencies = [ + "num-traits", + "rand", +] + +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + [[package]] name = "rayon" version = "1.10.0" @@ -3286,7 +3398,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3299,7 +3411,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.9.2", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3385,6 +3497,15 @@ version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" +[[package]] +name = "safe_arch" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b02de82ddbe1b636e6170c21be622223aea188ef2e139be0a5b219ec215323" +dependencies = [ + "bytemuck", +] + [[package]] name = "same-file" version = "1.0.6" @@ -3641,6 +3762,19 @@ dependencies = [ "libc", ] +[[package]] +name = "simba" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3a386a501cd104797982c15ae17aafe8b9261315b5d07e3ec803f2ea26be0fa" +dependencies = [ + "approx", + "num-complex", + "num-traits", + "paste", + "wide", +] + [[package]] name = "simd-adler32" version = "0.3.7" @@ -3720,6 +3854,18 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "statrs" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a3fe7c28c6512e766b0874335db33c94ad7b8f9054228ae1c2abd47ce7d335e" +dependencies = [ + "approx", + "nalgebra", + "num-traits", + "rand", +] + [[package]] name = "strict-num" version = "0.1.1" @@ -3881,7 +4027,7 @@ dependencies = [ "getrandom 0.3.1", "once_cell", "rustix 1.0.7", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -6216,6 +6362,16 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wide" +version = "0.7.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce5da8ecb62bcd8ec8b7ea19f69a51275e91299be594ea5cc6ef7819e16cd03" +dependencies = [ + "bytemuck", + "safe_arch", +] + [[package]] name = "widestring" version = "1.1.0" @@ -6244,7 +6400,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] diff --git a/crates/uv-bench/Cargo.toml b/crates/uv-bench/Cargo.toml index 5d55fafd7..afa73c32b 100644 --- a/crates/uv-bench/Cargo.toml +++ b/crates/uv-bench/Cargo.toml @@ -42,8 +42,10 @@ uv-types = { workspace = true } uv-workspace = { workspace = true } anyhow = { workspace = true } -codspeed-criterion-compat = { version = "2.7.2", default-features = false, optional = true } -criterion = { version = "0.6.0", default-features = false, features = ["async_tokio"] } +codspeed-criterion-compat = { version = "3.0.1", default-features = false, optional = true } +criterion = { version = "0.6.0", default-features = false, features = [ + "async_tokio", +] } jiff = { workspace = true } tokio = { workspace = true } From 5f8d4bbf02831d64595591fd3385131d162ca1e5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 30 Jun 2025 19:16:31 -0400 Subject: [PATCH 140/185] Update Rust crate indexmap to v2.10.0 (#14365) --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e317fd224..0c5bab5a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1896,9 +1896,9 @@ checksum = "b72ad49b554c1728b1e83254a1b1565aea4161e28dabbfa171fc15fe62299caf" [[package]] name = "indexmap" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" dependencies = [ "equivalent", "hashbrown 0.15.4", From 7bbdc08dae5218029c8ae1b3b2234c748129f882 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 30 Jun 2025 19:16:46 -0400 Subject: [PATCH 141/185] Update depot/build-push-action action to v1.15.0 (#14361) --- .github/workflows/build-docker.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml index 272a69e9a..843ee8dfb 100644 --- a/.github/workflows/build-docker.yml +++ b/.github/workflows/build-docker.yml @@ -137,7 +137,7 @@ jobs: - name: Build and push by digest id: build - uses: depot/build-push-action@636daae76684e38c301daa0c5eca1c095b24e780 # v1.14.0 + uses: depot/build-push-action@2583627a84956d07561420dcc1d0eb1f2af3fac0 # v1.15.0 with: project: 7hd4vdzmw5 # astral-sh/uv context: . @@ -267,7 +267,7 @@ jobs: - name: Build and push id: build-and-push - uses: depot/build-push-action@636daae76684e38c301daa0c5eca1c095b24e780 # v1.14.0 + uses: depot/build-push-action@2583627a84956d07561420dcc1d0eb1f2af3fac0 # v1.15.0 with: context: . project: 7hd4vdzmw5 # astral-sh/uv From c5ca240fb7b6956a9d375707917647b8c45d8446 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 30 Jun 2025 19:16:53 -0400 Subject: [PATCH 142/185] Update PyO3/maturin-action action to v1.49.3 (#14363) --- .github/workflows/build-binaries.yml | 44 ++++++++++++++-------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml index 0d0498453..ccd3ef3ee 100644 --- a/.github/workflows/build-binaries.yml +++ b/.github/workflows/build-binaries.yml @@ -54,7 +54,7 @@ jobs: - name: "Prep README.md" run: python scripts/transform_readme.py --target pypi - name: "Build sdist" - uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.1 + uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3 with: command: sdist args: --out dist @@ -74,7 +74,7 @@ jobs: # uv-build - name: "Build sdist uv-build" - uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.1 + uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3 with: command: sdist args: --out crates/uv-build/dist -m crates/uv-build/Cargo.toml @@ -103,7 +103,7 @@ jobs: # uv - name: "Build wheels - x86_64" - uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.1 + uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3 with: target: x86_64 args: --release --locked --out dist --features self-update @@ -133,7 +133,7 @@ jobs: # uv-build - name: "Build wheels uv-build - x86_64" - uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.1 + uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3 with: target: x86_64 args: --profile minimal-size --locked --out crates/uv-build/dist -m crates/uv-build/Cargo.toml @@ -157,7 +157,7 @@ jobs: # uv - name: "Build wheels - aarch64" - uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.1 + uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3 with: target: aarch64 args: --release --locked --out dist --features self-update @@ -193,7 +193,7 @@ jobs: # uv-build - name: "Build wheels uv-build - aarch64" - uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.1 + uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3 with: target: aarch64 args: --profile minimal-size --locked --out crates/uv-build/dist -m crates/uv-build/Cargo.toml @@ -231,7 +231,7 @@ jobs: # uv - name: "Build wheels" - uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.1 + uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3 with: target: ${{ matrix.platform.target }} args: --release --locked --out dist --features self-update,windows-gui-bin @@ -267,7 +267,7 @@ jobs: # uv-build - name: "Build wheels uv-build" - uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.1 + uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3 with: target: ${{ matrix.platform.target }} args: --profile minimal-size --locked --out crates/uv-build/dist -m crates/uv-build/Cargo.toml @@ -303,7 +303,7 @@ jobs: # uv - name: "Build wheels" - uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.1 + uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3 with: target: ${{ matrix.target }} # Generally, we try to build in a target docker container. In this case however, a @@ -368,7 +368,7 @@ jobs: # uv-build - name: "Build wheels uv-build" - uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.1 + uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3 with: target: ${{ matrix.target }} manylinux: auto @@ -412,7 +412,7 @@ jobs: # uv - name: "Build wheels" - uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.1 + uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3 with: target: ${{ matrix.platform.target }} # On `aarch64`, use `manylinux: 2_28`; otherwise, use `manylinux: auto`. @@ -461,7 +461,7 @@ jobs: # uv-build - name: "Build wheels uv-build" - uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.1 + uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3 with: target: ${{ matrix.platform.target }} # On `aarch64`, use `manylinux: 2_28`; otherwise, use `manylinux: auto`. @@ -509,7 +509,7 @@ jobs: # uv - name: "Build wheels" - uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.1 + uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3 with: target: ${{ matrix.platform.target }} manylinux: auto @@ -561,7 +561,7 @@ jobs: # uv-build - name: "Build wheels uv-build" - uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.1 + uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3 with: target: ${{ matrix.platform.target }} manylinux: auto @@ -614,7 +614,7 @@ jobs: # uv - name: "Build wheels" - uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.1 + uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3 with: target: ${{ matrix.platform.target }} manylinux: auto @@ -671,7 +671,7 @@ jobs: # uv-build - name: "Build wheels uv-build" - uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.1 + uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3 with: target: ${{ matrix.platform.target }} manylinux: auto @@ -712,7 +712,7 @@ jobs: # uv - name: "Build wheels" - uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.1 + uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3 with: target: ${{ matrix.platform.target }} manylinux: auto @@ -761,7 +761,7 @@ jobs: # uv-build - name: "Build wheels uv-build" - uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.1 + uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3 with: target: ${{ matrix.platform.target }} manylinux: auto @@ -807,7 +807,7 @@ jobs: # uv - name: "Build wheels" - uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.1 + uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3 with: target: ${{ matrix.target }} manylinux: musllinux_1_1 @@ -854,7 +854,7 @@ jobs: # uv-build - name: "Build wheels uv-build" - uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.1 + uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3 with: target: ${{ matrix.target }} manylinux: musllinux_1_1 @@ -901,7 +901,7 @@ jobs: # uv - name: "Build wheels" - uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.1 + uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3 with: target: ${{ matrix.platform.target }} manylinux: musllinux_1_1 @@ -966,7 +966,7 @@ jobs: # uv-build - name: "Build wheels" - uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.1 + uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3 with: target: ${{ matrix.platform.target }} manylinux: musllinux_1_1 From a3db9a9ae4a6232161ee55d6e5283aaa5e527342 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 1 Jul 2025 03:44:18 +0000 Subject: [PATCH 143/185] Sync latest Python releases (#14381) Automated update for Python releases. Co-authored-by: zanieb <2586601+zanieb@users.noreply.github.com> --- crates/uv-dev/src/generate_sysconfig_mappings.rs | 4 ++-- crates/uv-python/src/sysconfig/generated_mappings.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/uv-dev/src/generate_sysconfig_mappings.rs b/crates/uv-dev/src/generate_sysconfig_mappings.rs index 2e67f8e17..e35d2c3fc 100644 --- a/crates/uv-dev/src/generate_sysconfig_mappings.rs +++ b/crates/uv-dev/src/generate_sysconfig_mappings.rs @@ -11,7 +11,7 @@ use crate::ROOT_DIR; use crate::generate_all::Mode; /// Contains current supported targets -const TARGETS_YML_URL: &str = "https://raw.githubusercontent.com/astral-sh/python-build-standalone/refs/tags/20250626/cpython-unix/targets.yml"; +const TARGETS_YML_URL: &str = "https://raw.githubusercontent.com/astral-sh/python-build-standalone/refs/tags/20250630/cpython-unix/targets.yml"; #[derive(clap::Args)] pub(crate) struct Args { @@ -130,7 +130,7 @@ async fn generate() -> Result { output.push_str("//! DO NOT EDIT\n"); output.push_str("//!\n"); output.push_str("//! Generated with `cargo run dev generate-sysconfig-metadata`\n"); - output.push_str("//! Targets from \n"); + output.push_str("//! Targets from \n"); output.push_str("//!\n"); // Disable clippy/fmt diff --git a/crates/uv-python/src/sysconfig/generated_mappings.rs b/crates/uv-python/src/sysconfig/generated_mappings.rs index 1ae689833..850b2c764 100644 --- a/crates/uv-python/src/sysconfig/generated_mappings.rs +++ b/crates/uv-python/src/sysconfig/generated_mappings.rs @@ -1,7 +1,7 @@ //! DO NOT EDIT //! //! Generated with `cargo run dev generate-sysconfig-metadata` -//! Targets from +//! Targets from //! #![allow(clippy::all)] #![cfg_attr(any(), rustfmt::skip)] From b1812d111a77b29eef89dce2be32d90db43de0be Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Tue, 1 Jul 2025 03:44:23 -0500 Subject: [PATCH 144/185] Edits to the build backend documentation (#14376) Co-authored-by: konstin --- docs/concepts/build-backend.md | 210 +++++++++++++++++++++++---------- 1 file changed, 150 insertions(+), 60 deletions(-) diff --git a/docs/concepts/build-backend.md b/docs/concepts/build-backend.md index 78bc84636..dd7685756 100644 --- a/docs/concepts/build-backend.md +++ b/docs/concepts/build-backend.md @@ -7,98 +7,188 @@ When preview mode is not enabled, uv uses [hatchling](https://pypi.org/project/hatchling/) as the default build backend. A build backend transforms a source tree (i.e., a directory) into a source distribution or a wheel. -While uv supports all build backends (as specified by PEP 517), it includes a `uv_build` backend -that integrates tightly with uv to improve performance and user experience. -The uv build backend currently only supports Python code. An alternative backend is required if you -want to create a -[library with extension modules](../concepts/projects/init.md#projects-with-extension-modules). +uv supports all build backends (as specified by [PEP 517](https://peps.python.org/pep-0517/)), but +also provides a native build backend (`uv_build`) that integrates tightly with uv to improve +performance and user experience. -To use the uv build backend as [build system](../concepts/projects/config.md#build-systems) in an -existing project, add it to the `[build-system]` section in your `pyproject.toml`: +## Using the uv build backend -```toml +!!! important + + The uv build backend currently **only supports pure Python code**. An alternative backend is to + build a [library with extension modules](../concepts/projects/init.md#projects-with-extension-modules). + +To use uv as a build backend in an existing project, add `uv_build` to the +[`[build-system]`](../concepts/projects/config.md#build-systems) section in your `pyproject.toml`: + +```toml title="pyproject.toml" [build-system] requires = ["uv_build>=0.7.17,<0.8.0"] build-backend = "uv_build" ``` -!!! important +!!! note - The uv build backend follows the same [versioning policy](../reference/policies/versioning.md), - setting an upper bound on the `uv_build` version ensures that the package continues to build in - the future. + The uv build backend follows the same [versioning policy](../reference/policies/versioning.md) + as uv. Including an upper bound on the `uv_build` version ensures that your package continues to + build correctly as new versions are released. -You can also create a new project that uses the uv build backend with `uv init`: +To create a new project that uses the uv build backend, use `uv init`: -```shell -uv init --build-backend uv +```console +$ uv init --build-backend uv ``` -`uv_build` is a separate package from uv, optimized for portability and small binary size. The `uv` -command includes a copy of the build backend, so when running `uv build`, the same version will be -used for the build backend as for the uv process. Other build frontends, such as `python -m build`, -will choose the latest compatible `uv_build` version. +When the project is built, e.g., with [`uv build`](../guides/package.md), the uv build backend will +be used to create the source distribution and wheel. + +## Bundled build backend + +The build backend is published as a separate package (`uv_build`) that is optimized for portability +and small binary size. However, the `uv` executable also includes a copy of the build backend, which +will be used during builds performed by uv, e.g., during `uv build`, if its version is compatible +with the `uv_build` requirement. If it's not compatible, a compatible version of the `uv_build` +package will be used. Other build frontends, such as `python -m build`, will always use the +`uv_build` package, typically choosing the latest compatible version. ## Modules -The default module name is the package name in lower case with dots and dashes replaced by -underscores, and the default module location is under the `src` directory, i.e., the build backend -expects to find `src//__init__.py`. These defaults can be changed with the -`module-name` and `module-root` setting. The example below expects a module in the project root with -`PIL/__init__.py` instead: +Python packages are expected to contain one or more Python modules, which are directories containing +an `__init__.py`. By default, a single root module is expected at `src//__init__.py`. -```toml +For example, the structure for a project named `foo` would be: + +```text +pyproject.toml +src +└── foo + └── __init__.py +``` + +uv normalizes the package name to determine the default module name: the package name is lowercased +and dots and dashes are replaced with underscores, e.g., `Foo-Bar` would be converted to `foo_bar`. + +The `src/` directory is the default directory for module discovery. + +These defaults can be changed with the `module-name` and `module-root` settings. For example, to use +a `FOO` module in the root directory, as in the project structure: + +```text +pyproject.toml +FOO +└── __init__.py +``` + +The correct build configuration would be: + +```toml title="pyproject.toml" [tool.uv.build-backend] -module-name = "PIL" +module-name = "FOO" module-root = "" ``` -For a namespace packages, the path can be dotted. The example below expects to find a -`src/cloud/db/schema/__init__.py`: +## Namespace packages -```toml -[tool.uv.build-backend] -module-name = "cloud.db.schema" +Namespace packages are intended for use-cases where multiple packages write modules into a shared +namespace. + +Namespace package modules are identified by a `.` in the `module-name`. For example, to package the +module `bar` in the shared namespace `foo`, the project structure would be: + +```text +pyproject.toml +src +└── foo + └── bar + └── __init__.py ``` -Complex namespaces with more than one root module can be built by setting the `namespace` option, -which allows more than one root `__init__.py`: +And the `module-name` configuration would be: -```toml +```toml title="pyproject.toml" +[tool.uv.build-backend] +module-name = "foo.bar" +``` + +!!! important + + The `__init__.py` file is not included in `foo`, since it's the shared namespace module. + +It's also possible to have a complex namespace package with more than one root module, e.g., with +the project structure: + +```text +pyproject.toml +src +├── foo +│   └── __init__.py +└── bar + └── __init__.py +``` + +While we do not recommend this structure (i.e., you should use a workspace with multiple packages +instead), it is supported via the `namespace` option: + +```toml title="pyproject.toml" [tool.uv.build-backend] namespace = true ``` -The build backend supports building stubs packages with a `-stubs` suffix on the package or module -name, including for namespace packages. +## Stub packages -## Include and exclude configuration +The build backend also supports building type stub packages, which are identified by the `-stubs` +suffix on the package or module name, e.g., `foo-stubs`. The module name for type stub packages must +end in `-stubs`, so uv will not normalize the `-` to an underscore. Additionally, uv will search for +a `__init__.pyi` file. For example, the project structure would be: -To select which files to include in the source distribution, uv first adds the included files and +```text +pyproject.toml +src +└── foo-stubs + └── __init__.pyi +``` + +Type stub modules are also supported for [namespace packages](#namespace-packages). + +## File inclusion and exclusion + +The build backend is responsible for determining which files in a source tree should be packaged +into the distributions. + +To determine which files to include in a source distribution, uv first adds the included files and directories, then removes the excluded files and directories. This means that exclusions always take precedence over inclusions. -When building the source distribution, the following files and directories are included: +By default, uv excludes `__pycache__`, `*.pyc`, and `*.pyo`. -- `pyproject.toml` -- The module under `tool.uv.build-backend.module-root`, by default - `src//**`. -- `project.license-files` and `project.readme`. -- All directories under `tool.uv.build-backend.data`. -- All patterns from `tool.uv.build-backend.source-include`. +When building a source distribution, the following files and directories are included: -From these, `tool.uv.build-backend.source-exclude` and the default excludes are removed. +- The `pyproject.toml` +- The [module](#modules) under + [`tool.uv.build-backend.module-root`](../reference/settings.md#build-backend_module-root). +- The files referenced by `project.license-files` and `project.readme`. +- All directories under [`tool.uv.build-backend.data`](../reference/settings.md#build-backend_data). +- All files matching patterns from + [`tool.uv.build-backend.source-include`](../reference/settings.md#build-backend_source-include). -When building the wheel, the following files and directories are included: +From these, items matching +[`tool.uv.build-backend.source-exclude`](../reference/settings.md#build-backend_source-exclude) and +the [default excludes](../reference/settings.md#build-backend_default-excludes) are removed. -- The module under `tool.uv.build-backend.module-root`, by default - `src//**`. -- `project.license-files` and `project.readme`, as part of the project metadata. -- Each directory under `tool.uv.build-backend.data`, as data directories. +When building a wheel, the following files and directories are included: -From these, `tool.uv.build-backend.source-exclude`, `tool.uv.build-backend.wheel-exclude` and the -default excludes are removed. The source dist excludes are applied to avoid source tree to wheel +- The [module](#modules) under + [`tool.uv.build-backend.module-root`](../reference/settings.md#build-backend_module-root) +- The files referenced by `project.license-files`, which are copied into the `.dist-info` directory. +- The `project.readme`, which is copied into the project metadata. +- All directories under [`tool.uv.build-backend.data`](../reference/settings.md#build-backend_data), + which are copied into the `.data` directory. + +From these, +[`tool.uv.build-backend.source-exclude`](../reference/settings.md#build-backend_source-exclude), +[`tool.uv.build-backend.wheel-exclude`](../reference/settings.md#build-backend_wheel-exclude) and +the default excludes are removed. The source dist excludes are applied to avoid source tree to wheel source builds including more files than source tree to source distribution to wheel build. There are no specific wheel includes. There must only be one top level module, and all data files @@ -106,20 +196,20 @@ must either be under the module root or in the appropriate [data directory](../reference/settings.md#build-backend_data). Most packages store small data in the module root alongside the source code. -## Include and exclude syntax +### Include and exclude syntax -Includes are anchored, which means that `pyproject.toml` includes only -`/pyproject.toml`. For example, `assets/**/sample.csv` includes all `sample.csv` files -in `/assets` or any child directory. To recursively include all files under a -directory, use a `/**` suffix, e.g. `src/**`. +Includes are anchored, which means that `pyproject.toml` includes only `/pyproject.toml` and +not `/bar/pyproject.toml`. To recursively include all files under a directory, use a `/**` +suffix, e.g. `src/**`. Recursive inclusions are also anchored, e.g., `assets/**/sample.csv` includes +all `sample.csv` files in `/assets` or any of its children. !!! note For performance and reproducibility, avoid patterns without an anchor such as `**/sample.csv`. Excludes are not anchored, which means that `__pycache__` excludes all directories named -`__pycache__` and its children anywhere. To anchor a directory, use a `/` prefix, e.g., `/dist` will -exclude only `/dist`. +`__pycache__` regardless of its parent directory. All children of an exclusion are excluded as well. +To anchor a directory, use a `/` prefix, e.g., `/dist` will exclude only `/dist`. All fields accepting patterns use the reduced portable glob syntax from [PEP 639](https://peps.python.org/pep-0639/#add-license-FILES-key), with the addition that From 3774a656d7c864370408b8c253909af3b83296d5 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 1 Jul 2025 08:18:01 -0400 Subject: [PATCH 145/185] Use parsed URLs for conflicting URL error message (#14380) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary There's a good example of the downside of using verbatim URLs here: https://github.com/astral-sh/uv/pull/14197#discussion_r2163599625 (we show two relative paths that point to the same directory, but it's not clear from the error message). The diff: ``` 2 2 │ ----- stdout ----- 3 3 │ 4 4 │ ----- stderr ----- 5 5 │ error: Requirements contain conflicting URLs for package `library` in all marker environments: 6 │-- ../../library 7 │-- ./library 6 │+- file://[TEMP_DIR]/library 7 │+- file://[TEMP_DIR]/library (editable) ``` --- crates/uv-resolver/src/error.rs | 16 ++++++++++++---- crates/uv-resolver/src/fork_indexes.rs | 2 +- crates/uv-resolver/src/fork_urls.rs | 7 ++----- crates/uv-resolver/src/resolver/urls.rs | 5 ++--- crates/uv/tests/it/pip_compile.rs | 6 +++--- crates/uv/tests/it/pip_install.rs | 6 +++--- 6 files changed, 23 insertions(+), 19 deletions(-) diff --git a/crates/uv-resolver/src/error.rs b/crates/uv-resolver/src/error.rs index 2033ed0c0..e327e8562 100644 --- a/crates/uv-resolver/src/error.rs +++ b/crates/uv-resolver/src/error.rs @@ -17,6 +17,8 @@ use uv_normalize::{ExtraName, InvalidNameError, PackageName}; use uv_pep440::{LocalVersionSlice, LowerBound, Version, VersionSpecifier}; use uv_pep508::{MarkerEnvironment, MarkerExpression, MarkerTree, MarkerValueVersion}; use uv_platform_tags::Tags; +use uv_pypi_types::ParsedUrl; +use uv_redacted::DisplaySafeUrl; use uv_static::EnvVars; use crate::candidate_selector::CandidateSelector; @@ -56,11 +58,14 @@ pub enum ResolveError { } else { format!(" in {env}") }, - urls.join("\n- "), + urls.iter() + .map(|url| format!("{}{}", DisplaySafeUrl::from(url.clone()), if url.is_editable() { " (editable)" } else { "" })) + .collect::>() + .join("\n- ") )] ConflictingUrls { package_name: PackageName, - urls: Vec, + urls: Vec, env: ResolverEnvironment, }, @@ -71,11 +76,14 @@ pub enum ResolveError { } else { format!(" in {env}") }, - indexes.join("\n- "), + indexes.iter() + .map(std::string::ToString::to_string) + .collect::>() + .join("\n- ") )] ConflictingIndexesForEnvironment { package_name: PackageName, - indexes: Vec, + indexes: Vec, env: ResolverEnvironment, }, diff --git a/crates/uv-resolver/src/fork_indexes.rs b/crates/uv-resolver/src/fork_indexes.rs index 5b39fb626..7283b5cbc 100644 --- a/crates/uv-resolver/src/fork_indexes.rs +++ b/crates/uv-resolver/src/fork_indexes.rs @@ -24,7 +24,7 @@ impl ForkIndexes { ) -> Result<(), ResolveError> { if let Some(previous) = self.0.insert(package_name.clone(), index.clone()) { if &previous != index { - let mut conflicts = vec![previous.url.to_string(), index.url.to_string()]; + let mut conflicts = vec![previous.url, index.url.clone()]; conflicts.sort(); return Err(ResolveError::ConflictingIndexesForEnvironment { package_name: package_name.clone(), diff --git a/crates/uv-resolver/src/fork_urls.rs b/crates/uv-resolver/src/fork_urls.rs index dc1b067c4..dd69f7bf7 100644 --- a/crates/uv-resolver/src/fork_urls.rs +++ b/crates/uv-resolver/src/fork_urls.rs @@ -2,7 +2,6 @@ use std::collections::hash_map::Entry; use rustc_hash::FxHashMap; -use uv_distribution_types::Verbatim; use uv_normalize::PackageName; use uv_pypi_types::VerbatimParsedUrl; @@ -34,10 +33,8 @@ impl ForkUrls { match self.0.entry(package_name.clone()) { Entry::Occupied(previous) => { if previous.get() != url { - let mut conflicting_url = vec![ - previous.get().verbatim.verbatim().to_string(), - url.verbatim.verbatim().to_string(), - ]; + let mut conflicting_url = + vec![previous.get().parsed_url.clone(), url.parsed_url.clone()]; conflicting_url.sort(); return Err(ResolveError::ConflictingUrls { package_name: package_name.clone(), diff --git a/crates/uv-resolver/src/resolver/urls.rs b/crates/uv-resolver/src/resolver/urls.rs index a41f33371..73d190b4a 100644 --- a/crates/uv-resolver/src/resolver/urls.rs +++ b/crates/uv-resolver/src/resolver/urls.rs @@ -4,7 +4,6 @@ use same_file::is_same_file; use tracing::debug; use uv_cache_key::CanonicalUrl; -use uv_distribution_types::Verbatim; use uv_git::GitResolver; use uv_normalize::PackageName; use uv_pep508::{MarkerTree, VerbatimUrl}; @@ -170,8 +169,8 @@ impl Urls { let [allowed_url] = matching_urls.as_slice() else { let mut conflicting_urls: Vec<_> = matching_urls .into_iter() - .map(|parsed_url| parsed_url.verbatim.verbatim().to_string()) - .chain(std::iter::once(verbatim_url.verbatim().to_string())) + .map(|parsed_url| parsed_url.parsed_url.clone()) + .chain(std::iter::once(parsed_url.clone())) .collect(); conflicting_urls.sort(); return Err(ResolveError::ConflictingUrls { diff --git a/crates/uv/tests/it/pip_compile.rs b/crates/uv/tests/it/pip_compile.rs index 79a98a3bf..c80027761 100644 --- a/crates/uv/tests/it/pip_compile.rs +++ b/crates/uv/tests/it/pip_compile.rs @@ -2909,16 +2909,16 @@ fn incompatible_narrowed_url_dependency() -> Result<()> { "})?; uv_snapshot!(context.filters(), context.pip_compile() - .arg("requirements.in"), @r###" + .arg("requirements.in"), @r" success: false exit_code: 2 ----- stdout ----- ----- stderr ----- error: Requirements contain conflicting URLs for package `uv-public-pypackage`: - - git+https://github.com/astral-test/uv-public-pypackage@b270df1a2fb5d012294e9aaf05e7e0bab1e6a389 - git+https://github.com/astral-test/uv-public-pypackage@test-branch - "### + - git+https://github.com/astral-test/uv-public-pypackage@b270df1a2fb5d012294e9aaf05e7e0bab1e6a389 + " ); Ok(()) diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index 090fb03a9..91da3ce81 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -1515,16 +1515,16 @@ fn install_editable_incompatible_constraint_url() -> Result<()> { .arg("-e") .arg(context.workspace_root.join("scripts/packages/black_editable")) .arg("--constraint") - .arg("constraints.txt"), @r###" + .arg("constraints.txt"), @r" success: false exit_code: 2 ----- stdout ----- ----- stderr ----- error: Requirements contain conflicting URLs for package `black`: - - [WORKSPACE]/scripts/packages/black_editable + - file://[WORKSPACE]/scripts/packages/black_editable (editable) - https://files.pythonhosted.org/packages/0f/89/294c9a6b6c75a08da55e9d05321d0707e9418735e3062b12ef0f54c33474/black-24.4.2-py3-none-any.whl - "### + " ); Ok(()) From 43745d2ecfd34c07e17d952d982e8ef6a3056bcb Mon Sep 17 00:00:00 2001 From: konsti Date: Tue, 1 Jul 2025 17:48:48 +0200 Subject: [PATCH 146/185] Fix equals-star and tilde-equals with `python_version` and `python_full_version` (#14271) The marker display code assumes that all versions are normalized, in that all trailing zeroes are stripped. This is not the case for tilde-equals and equals-star versions, where the trailing zeroes (before the `.*`) are semantically relevant. This would cause path dependent-behavior where we would get a different marker string depending on whether a version with or without a trailing zero was added to the cache first. To handle both equals-star and tilde-equals when converting `python_version` to `python_full_version` markers, we have to merge the version normalization (i.e. trimming the trailing zeroes) and the conversion both to `python_full_version` and to `Ranges`, while special casing equals-star and tilde-equals. To avoid churn in lockfiles, we only trim in the conversion to `Ranges` for markers, but keep using untrimmed versions for requires-python. (Note that this behavior is technically also path dependent, as versions with and without trailing zeroes have the same Hash and Eq. E.q., `requires-python == ">= 3.10.0"` and `requires-python == ">= 3.10"` in the same workspace could lead to either value in `uv.lock`, and which one it is could change if we make unrelated (performance) changes. Always trimming however definitely changes lockfiles, a churn I wouldn't do outside another breaking or lockfile-changing change.) Nevertheless, there is a change for users who have `requires-python = "~= 3.12.0"` in their `pyproject.toml`, as this now hits the correct normalization path. Fixes #14231 Fixes #14270 --- .../src/requires_python.rs | 6 +- crates/uv-pep440/src/version.rs | 18 +++ crates/uv-pep440/src/version_ranges.rs | 81 ++++++------ crates/uv-pep440/src/version_specifier.rs | 92 ++++++++++--- crates/uv-pep508/src/lib.rs | 1 + crates/uv-pep508/src/marker/algebra.rs | 122 ++++++++++-------- crates/uv-pep508/src/marker/simplify.rs | 42 +++--- crates/uv-pep508/src/marker/tree.rs | 117 +++++++++++++++-- crates/uv-pypi-types/src/conflicts.rs | 4 +- crates/uv-pypi-types/src/identifier.rs | 1 + ...__line-endings-poetry-with-hashes.txt.snap | 11 +- ...t__test__parse-poetry-with-hashes.txt.snap | 11 +- crates/uv/tests/it/lock.rs | 39 +++++- 13 files changed, 382 insertions(+), 163 deletions(-) diff --git a/crates/uv-distribution-types/src/requires_python.rs b/crates/uv-distribution-types/src/requires_python.rs index 786aed83a..cedfff843 100644 --- a/crates/uv-distribution-types/src/requires_python.rs +++ b/crates/uv-distribution-types/src/requires_python.rs @@ -71,7 +71,7 @@ impl RequiresPython { // Warn if there’s exactly one `~=` specifier without a patch. if let [spec] = &specs[..] { if spec.is_tilde_without_patch() { - if let Some((lo_b, hi_b)) = release_specifier_to_range(spec.clone()) + if let Some((lo_b, hi_b)) = release_specifier_to_range(spec.clone(), false) .bounding_range() .map(|(l, u)| (l.cloned(), u.cloned())) { @@ -80,8 +80,8 @@ impl RequiresPython { warn_user_once!( "The release specifier (`{spec}`) contains a compatible release \ match without a patch version. This will be interpreted as \ - `{lo_spec}, {hi_spec}`. Did you mean `{spec}.0` to freeze the minor \ - version?" + `{lo_spec}, {hi_spec}`. Did you mean `{spec}.0` to freeze the \ + minor version?" ); } } diff --git a/crates/uv-pep440/src/version.rs b/crates/uv-pep440/src/version.rs index 1ef0badf2..a496f95a2 100644 --- a/crates/uv-pep440/src/version.rs +++ b/crates/uv-pep440/src/version.rs @@ -610,6 +610,24 @@ impl Version { Self::new(self.release().iter().copied()) } + /// Return the version with any segments apart from the release removed, with trailing zeroes + /// trimmed. + #[inline] + #[must_use] + pub fn only_release_trimmed(&self) -> Self { + if let Some(last_non_zero) = self.release().iter().rposition(|segment| *segment != 0) { + if last_non_zero == self.release().len() { + // Already trimmed. + self.clone() + } else { + Self::new(self.release().iter().take(last_non_zero + 1).copied()) + } + } else { + // `0` is a valid version. + Self::new([0]) + } + } + /// Return the version with trailing `.0` release segments removed. /// /// # Panics diff --git a/crates/uv-pep440/src/version_ranges.rs b/crates/uv-pep440/src/version_ranges.rs index 26cd048d3..38038ffcf 100644 --- a/crates/uv-pep440/src/version_ranges.rs +++ b/crates/uv-pep440/src/version_ranges.rs @@ -130,10 +130,11 @@ impl From for Ranges { /// /// See: pub fn release_specifiers_to_ranges(specifiers: VersionSpecifiers) -> Ranges { - specifiers - .into_iter() - .map(release_specifier_to_range) - .fold(Ranges::full(), |acc, range| acc.intersection(&range)) + let mut range = Ranges::full(); + for specifier in specifiers { + range = range.intersection(&release_specifier_to_range(specifier, false)); + } + range } /// Convert the [`VersionSpecifier`] to a PubGrub-compatible version range, using release-only @@ -147,67 +148,57 @@ pub fn release_specifiers_to_ranges(specifiers: VersionSpecifiers) -> Ranges -pub fn release_specifier_to_range(specifier: VersionSpecifier) -> Ranges { +pub fn release_specifier_to_range(specifier: VersionSpecifier, trim: bool) -> Ranges { let VersionSpecifier { operator, version } = specifier; + // Note(konsti): We switched strategies to trimmed for the markers, but we don't want to cause + // churn in lockfile requires-python, so we only trim for markers. + let version_trimmed = if trim { + version.only_release_trimmed() + } else { + version.only_release() + }; match operator { - Operator::Equal => { - let version = version.only_release(); - Ranges::singleton(version) - } - Operator::ExactEqual => { - let version = version.only_release(); - Ranges::singleton(version) - } - Operator::NotEqual => { - let version = version.only_release(); - Ranges::singleton(version).complement() - } + // Trailing zeroes are not semantically relevant. + Operator::Equal => Ranges::singleton(version_trimmed), + Operator::ExactEqual => Ranges::singleton(version_trimmed), + Operator::NotEqual => Ranges::singleton(version_trimmed).complement(), + Operator::LessThan => Ranges::strictly_lower_than(version_trimmed), + Operator::LessThanEqual => Ranges::lower_than(version_trimmed), + Operator::GreaterThan => Ranges::strictly_higher_than(version_trimmed), + Operator::GreaterThanEqual => Ranges::higher_than(version_trimmed), + + // Trailing zeroes are semantically relevant. Operator::TildeEqual => { let release = version.release(); let [rest @ .., last, _] = &*release else { unreachable!("~= must have at least two segments"); }; let upper = Version::new(rest.iter().chain([&(last + 1)])); - let version = version.only_release(); - Ranges::from_range_bounds(version..upper) - } - Operator::LessThan => { - let version = version.only_release(); - Ranges::strictly_lower_than(version) - } - Operator::LessThanEqual => { - let version = version.only_release(); - Ranges::lower_than(version) - } - Operator::GreaterThan => { - let version = version.only_release(); - Ranges::strictly_higher_than(version) - } - Operator::GreaterThanEqual => { - let version = version.only_release(); - Ranges::higher_than(version) + Ranges::from_range_bounds(version_trimmed..upper) } Operator::EqualStar => { - let low = version.only_release(); + // For (not-)equal-star, trailing zeroes are still before the star. + let low_full = version.only_release(); let high = { - let mut high = low.clone(); + let mut high = low_full.clone(); let mut release = high.release().to_vec(); *release.last_mut().unwrap() += 1; high = high.with_release(release); high }; - Ranges::from_range_bounds(low..high) + Ranges::from_range_bounds(version..high) } Operator::NotEqualStar => { - let low = version.only_release(); + // For (not-)equal-star, trailing zeroes are still before the star. + let low_full = version.only_release(); let high = { - let mut high = low.clone(); + let mut high = low_full.clone(); let mut release = high.release().to_vec(); *release.last_mut().unwrap() += 1; high = high.with_release(release); high }; - Ranges::from_range_bounds(low..high).complement() + Ranges::from_range_bounds(version..high).complement() } } } @@ -222,8 +213,8 @@ impl LowerBound { /// These bounds use release-only semantics when comparing versions. pub fn new(bound: Bound) -> Self { Self(match bound { - Bound::Included(version) => Bound::Included(version.only_release()), - Bound::Excluded(version) => Bound::Excluded(version.only_release()), + Bound::Included(version) => Bound::Included(version.only_release_trimmed()), + Bound::Excluded(version) => Bound::Excluded(version.only_release_trimmed()), Bound::Unbounded => Bound::Unbounded, }) } @@ -357,8 +348,8 @@ impl UpperBound { /// These bounds use release-only semantics when comparing versions. pub fn new(bound: Bound) -> Self { Self(match bound { - Bound::Included(version) => Bound::Included(version.only_release()), - Bound::Excluded(version) => Bound::Excluded(version.only_release()), + Bound::Included(version) => Bound::Included(version.only_release_trimmed()), + Bound::Excluded(version) => Bound::Excluded(version.only_release_trimmed()), Bound::Unbounded => Bound::Unbounded, }) } diff --git a/crates/uv-pep440/src/version_specifier.rs b/crates/uv-pep440/src/version_specifier.rs index 19acff2eb..bbbe440bf 100644 --- a/crates/uv-pep440/src/version_specifier.rs +++ b/crates/uv-pep440/src/version_specifier.rs @@ -80,24 +80,38 @@ impl VersionSpecifiers { // Add specifiers for the holes between the bounds. for (lower, upper) in bounds { - match (next, lower) { + let specifier = match (next, lower) { // Ex) [3.7, 3.8.5), (3.8.5, 3.9] -> >=3.7,!=3.8.5,<=3.9 (Bound::Excluded(prev), Bound::Excluded(lower)) if prev == lower => { - specifiers.push(VersionSpecifier::not_equals_version(prev.clone())); + Some(VersionSpecifier::not_equals_version(prev.clone())) } // Ex) [3.7, 3.8), (3.8, 3.9] -> >=3.7,!=3.8.*,<=3.9 - (Bound::Excluded(prev), Bound::Included(lower)) - if prev.release().len() == 2 - && *lower.release() == [prev.release()[0], prev.release()[1] + 1] => - { - specifiers.push(VersionSpecifier::not_equals_star_version(prev.clone())); - } - _ => { - #[cfg(feature = "tracing")] - warn!( - "Ignoring unsupported gap in `requires-python` version: {next:?} -> {lower:?}" - ); + (Bound::Excluded(prev), Bound::Included(lower)) => { + match *prev.only_release_trimmed().release() { + [major] if *lower.only_release_trimmed().release() == [major, 1] => { + Some(VersionSpecifier::not_equals_star_version(Version::new([ + major, 0, + ]))) + } + [major, minor] + if *lower.only_release_trimmed().release() == [major, minor + 1] => + { + Some(VersionSpecifier::not_equals_star_version(Version::new([ + major, minor, + ]))) + } + _ => None, + } } + _ => None, + }; + if let Some(specifier) = specifier { + specifiers.push(specifier); + } else { + #[cfg(feature = "tracing")] + warn!( + "Ignoring unsupported gap in `requires-python` version: {next:?} -> {lower:?}" + ); } next = upper; } @@ -348,6 +362,33 @@ impl VersionSpecifier { Ok(Self { operator, version }) } + /// Remove all non-release parts of the version. + /// + /// The marker decision diagram relies on the assumption that the negation of a marker tree is + /// the complement of the marker space. However, pre-release versions violate this assumption. + /// + /// For example, the marker `python_full_version > '3.9' or python_full_version <= '3.9'` + /// does not match `python_full_version == 3.9.0a0` and so cannot simplify to `true`. However, + /// its negation, `python_full_version > '3.9' and python_full_version <= '3.9'`, also does not + /// match `3.9.0a0` and simplifies to `false`, which violates the algebra decision diagrams + /// rely on. For this reason we ignore pre-release versions entirely when evaluating markers. + /// + /// Note that `python_version` cannot take on pre-release values as it is truncated to just the + /// major and minor version segments. Thus using release-only specifiers is definitely necessary + /// for `python_version` to fully simplify any ranges, such as + /// `python_version > '3.9' or python_version <= '3.9'`, which is always `true` for + /// `python_version`. For `python_full_version` however, this decision is a semantic change. + /// + /// For Python versions, the major.minor is considered the API version, so unlike the rules + /// for package versions in PEP 440, we Python `3.9.0a0` is acceptable for `>= "3.9"`. + #[must_use] + pub fn only_release(self) -> Self { + Self { + operator: self.operator, + version: self.version.only_release(), + } + } + /// `==` pub fn equals_version(version: Version) -> Self { Self { @@ -442,14 +483,23 @@ impl VersionSpecifier { (Some(VersionSpecifier::equals_version(v1.clone())), None) } // `v >= 3.7 && v < 3.8` is equivalent to `v == 3.7.*` - (Bound::Included(v1), Bound::Excluded(v2)) - if v1.release().len() == 2 - && *v2.release() == [v1.release()[0], v1.release()[1] + 1] => - { - ( - Some(VersionSpecifier::equals_star_version(v1.clone())), - None, - ) + (Bound::Included(v1), Bound::Excluded(v2)) => { + match *v1.only_release_trimmed().release() { + [major] if *v2.only_release_trimmed().release() == [major, 1] => { + let version = Version::new([major, 0]); + (Some(VersionSpecifier::equals_star_version(version)), None) + } + [major, minor] + if *v2.only_release_trimmed().release() == [major, minor + 1] => + { + let version = Version::new([major, minor]); + (Some(VersionSpecifier::equals_star_version(version)), None) + } + _ => ( + VersionSpecifier::from_lower_bound(&Bound::Included(v1.clone())), + VersionSpecifier::from_upper_bound(&Bound::Excluded(v2.clone())), + ), + } } (lower, upper) => ( VersionSpecifier::from_lower_bound(lower), diff --git a/crates/uv-pep508/src/lib.rs b/crates/uv-pep508/src/lib.rs index 46e2f3039..e2945743b 100644 --- a/crates/uv-pep508/src/lib.rs +++ b/crates/uv-pep508/src/lib.rs @@ -16,6 +16,7 @@ #![warn(missing_docs)] +#[cfg(feature = "schemars")] use std::borrow::Cow; use std::error::Error; use std::fmt::{Debug, Display, Formatter}; diff --git a/crates/uv-pep508/src/marker/algebra.rs b/crates/uv-pep508/src/marker/algebra.rs index f421a8fa3..2a3f82f27 100644 --- a/crates/uv-pep508/src/marker/algebra.rs +++ b/crates/uv-pep508/src/marker/algebra.rs @@ -172,7 +172,7 @@ impl InternerGuard<'_> { ), // Normalize `python_version` markers to `python_full_version` nodes. MarkerValueVersion::PythonVersion => { - match python_version_to_full_version(normalize_specifier(specifier)) { + match python_version_to_full_version(specifier.only_release()) { Ok(specifier) => ( Variable::Version(CanonicalMarkerValueVersion::PythonFullVersion), Edges::from_specifier(specifier), @@ -1214,7 +1214,7 @@ impl Edges { /// Returns the [`Edges`] for a version specifier. fn from_specifier(specifier: VersionSpecifier) -> Edges { - let specifier = release_specifier_to_range(normalize_specifier(specifier)); + let specifier = release_specifier_to_range(specifier.only_release(), true); Edges::Version { edges: Edges::from_range(&specifier), } @@ -1227,9 +1227,9 @@ impl Edges { let mut range: Ranges = versions .into_iter() .map(|version| { - let specifier = VersionSpecifier::equals_version(version.clone()); + let specifier = VersionSpecifier::equals_version(version.only_release()); let specifier = python_version_to_full_version(specifier)?; - Ok(release_specifier_to_range(normalize_specifier(specifier))) + Ok(release_specifier_to_range(specifier, true)) }) .flatten_ok() .collect::, NodeId>>()?; @@ -1526,57 +1526,62 @@ impl Edges { } } -// Normalize a [`VersionSpecifier`] before adding it to the tree. -fn normalize_specifier(specifier: VersionSpecifier) -> VersionSpecifier { - let (operator, version) = specifier.into_parts(); - - // The decision diagram relies on the assumption that the negation of a marker tree is - // the complement of the marker space. However, pre-release versions violate this assumption. - // - // For example, the marker `python_full_version > '3.9' or python_full_version <= '3.9'` - // does not match `python_full_version == 3.9.0a0` and so cannot simplify to `true`. However, - // its negation, `python_full_version > '3.9' and python_full_version <= '3.9'`, also does not - // match `3.9.0a0` and simplifies to `false`, which violates the algebra decision diagrams - // rely on. For this reason we ignore pre-release versions entirely when evaluating markers. - // - // Note that `python_version` cannot take on pre-release values as it is truncated to just the - // major and minor version segments. Thus using release-only specifiers is definitely necessary - // for `python_version` to fully simplify any ranges, such as `python_version > '3.9' or python_version <= '3.9'`, - // which is always `true` for `python_version`. For `python_full_version` however, this decision - // is a semantic change. - let mut release = &*version.release(); - - // Strip any trailing `0`s. - // - // The [`Version`] type ignores trailing `0`s for equality, but still preserves them in its - // [`Display`] output. We must normalize all versions by stripping trailing `0`s to remove the - // distinction between versions like `3.9` and `3.9.0`. Otherwise, their output would depend on - // which form was added to the global marker interner first. - // - // Note that we cannot strip trailing `0`s for star equality, as `==3.0.*` is different from `==3.*`. - if !operator.is_star() { - if let Some(end) = release.iter().rposition(|segment| *segment != 0) { - if end > 0 { - release = &release[..=end]; - } - } - } - - VersionSpecifier::from_version(operator, Version::new(release)).unwrap() -} - /// Returns the equivalent `python_full_version` specifier for a `python_version` specifier. /// /// Returns `Err` with a constant node if the equivalent comparison is always `true` or `false`. fn python_version_to_full_version(specifier: VersionSpecifier) -> Result { + // Trailing zeroes matter only for (not-)equals-star and tilde-equals. This means that below + // the next two blocks, we can use the trimmed release as the release. + if specifier.operator().is_star() { + // Input python_version python_full_version + // ==3.* 3.* 3.* + // ==3.0.* 3.0 3.0.* + // ==3.0.0.* 3.0 3.0.* + // ==3.9.* 3.9 3.9.* + // ==3.9.0.* 3.9 3.9.* + // ==3.9.0.0.* 3.9 3.9.* + // ==3.9.1.* FALSE FALSE + // ==3.9.1.0.* FALSE FALSE + // ==3.9.1.0.0.* FALSE FALSE + return match &*specifier.version().release() { + // `3.*` + [_major] => Ok(specifier), + // Ex) `3.9.*`, `3.9.0.*`, or `3.9.0.0.*` + [major, minor, rest @ ..] if rest.iter().all(|x| *x == 0) => { + let python_version = Version::new([major, minor]); + // Unwrap safety: A star operator with two version segments is always valid. + Ok(VersionSpecifier::from_version(*specifier.operator(), python_version).unwrap()) + } + // Ex) `3.9.1.*` or `3.9.0.1.*` + _ => Err(NodeId::FALSE), + }; + } + + if *specifier.operator() == Operator::TildeEqual { + // python_version python_full_version + // ~=3 (not possible) + // ~= 3.0 >= 3.0, < 4.0 + // ~= 3.9 >= 3.9, < 4.0 + // ~= 3.9.0 == 3.9.* + // ~= 3.9.1 FALSE + // ~= 3.9.0.0 == 3.9.* + // ~= 3.9.0.1 FALSE + return match &*specifier.version().release() { + // Ex) `3.0`, `3.7` + [_major, _minor] => Ok(specifier), + // Ex) `3.9`, `3.9.0`, or `3.9.0.0` + [major, minor, rest @ ..] if rest.iter().all(|x| *x == 0) => { + let python_version = Version::new([major, minor]); + Ok(VersionSpecifier::equals_star_version(python_version)) + } + // Ex) `3.9.1` or `3.9.0.1` + _ => Err(NodeId::FALSE), + }; + } + // Extract the major and minor version segments if the specifier contains exactly // those segments, or if it contains a major segment with an implied minor segment of `0`. - let major_minor = match *specifier.version().release() { - // For star operators, we cannot add a trailing `0`. - // - // `python_version == 3.*` is equivalent to `python_full_version == 3.*`. Adding a - // trailing `0` would result in `python_version == 3.0.*`, which is incorrect. - [_major] if specifier.operator().is_star() => return Ok(specifier), + let major_minor = match *specifier.version().only_release_trimmed().release() { // Add a trailing `0` for the minor version, which is implied. // For example, `python_version == 3` matches `3.0.1`, `3.0.2`, etc. [major] => Some((major, 0)), @@ -1614,9 +1619,10 @@ fn python_version_to_full_version(specifier: VersionSpecifier) -> Result specifier, + Operator::EqualStar | Operator::NotEqualStar | Operator::TildeEqual => { + // Handled above. + unreachable!() + } }) } else { let [major, minor, ..] = *specifier.version().release() else { @@ -1624,13 +1630,14 @@ fn python_version_to_full_version(specifier: VersionSpecifier) -> Result { + // `python_version` cannot have more than two release segments, and we know + // that the following release segments aren't purely zeroes so equality is impossible. + Operator::Equal | Operator::ExactEqual => { return Err(NodeId::FALSE); } // Similarly, inequalities are always `true`. - Operator::NotEqual | Operator::NotEqualStar => return Err(NodeId::TRUE), + Operator::NotEqual => return Err(NodeId::TRUE), // `python_version {<,<=} 3.7.8` is equivalent to `python_full_version < 3.8`. Operator::LessThan | Operator::LessThanEqual => { @@ -1641,6 +1648,11 @@ fn python_version_to_full_version(specifier: VersionSpecifier) -> Result { VersionSpecifier::greater_than_equal_version(Version::new([major, minor + 1])) } + + Operator::EqualStar | Operator::NotEqualStar | Operator::TildeEqual => { + // Handled above. + unreachable!() + } }) } } diff --git a/crates/uv-pep508/src/marker/simplify.rs b/crates/uv-pep508/src/marker/simplify.rs index 34c095b09..3dc03693a 100644 --- a/crates/uv-pep508/src/marker/simplify.rs +++ b/crates/uv-pep508/src/marker/simplify.rs @@ -64,8 +64,8 @@ fn collect_dnf( continue; } - // Detect whether the range for this edge can be simplified as a star inequality. - if let Some(specifier) = star_range_inequality(&range) { + // Detect whether the range for this edge can be simplified as a star specifier. + if let Some(specifier) = star_range_specifier(&range) { path.push(MarkerExpression::Version { key: marker.key().into(), specifier, @@ -343,22 +343,34 @@ where Some(excluded) } -/// Returns `Some` if the version expression can be simplified as a star inequality with the given -/// specifier. +/// Returns `Some` if the version range can be simplified as a star specifier. /// -/// For example, `python_full_version < '3.8' or python_full_version >= '3.9'` can be simplified to -/// `python_full_version != '3.8.*'`. -fn star_range_inequality(range: &Ranges) -> Option { +/// Only for the two bounds case not covered by [`VersionSpecifier::from_release_only_bounds`]. +/// +/// For negative ranges like `python_full_version < '3.8' or python_full_version >= '3.9'`, +/// returns `!= '3.8.*'`. +fn star_range_specifier(range: &Ranges) -> Option { + if range.iter().count() != 2 { + return None; + } + // Check for negative star range: two segments [(Unbounded, Excluded(v1)), (Included(v2), Unbounded)] let (b1, b2) = range.iter().collect_tuple()?; - - match (b1, b2) { - ((Bound::Unbounded, Bound::Excluded(v1)), (Bound::Included(v2), Bound::Unbounded)) - if v1.release().len() == 2 - && *v2.release() == [v1.release()[0], v1.release()[1] + 1] => - { - Some(VersionSpecifier::not_equals_star_version(v1.clone())) + if let ((Bound::Unbounded, Bound::Excluded(v1)), (Bound::Included(v2), Bound::Unbounded)) = + (b1, b2) + { + match *v1.only_release_trimmed().release() { + [major] if *v2.release() == [major, 1] => { + Some(VersionSpecifier::not_equals_star_version(Version::new([ + major, 0, + ]))) + } + [major, minor] if *v2.release() == [major, minor + 1] => { + Some(VersionSpecifier::not_equals_star_version(v1.clone())) + } + _ => None, } - _ => None, + } else { + None } } diff --git a/crates/uv-pep508/src/marker/tree.rs b/crates/uv-pep508/src/marker/tree.rs index 975d4ff10..5739d7c98 100644 --- a/crates/uv-pep508/src/marker/tree.rs +++ b/crates/uv-pep508/src/marker/tree.rs @@ -2271,13 +2271,13 @@ mod test { #[test] fn test_marker_simplification() { assert_false("python_version == '3.9.1'"); - assert_false("python_version == '3.9.0.*'"); assert_true("python_version != '3.9.1'"); - // Technically these is are valid substring comparison, but we do not allow them. - // e.g., using a version with patch components with `python_version` is considered - // impossible to satisfy since the value it is truncated at the minor version - assert_false("python_version in '3.9.0'"); + // This is an edge case that happens to be supported, but is not critical to support. + assert_simplifies( + "python_version in '3.9.0'", + "python_full_version == '3.9.*'", + ); // e.g., using a version that is not PEP 440 compliant is considered arbitrary assert_true("python_version in 'foo'"); // e.g., including `*` versions, which would require tracking a version specifier @@ -2287,16 +2287,25 @@ mod test { assert_true("python_version in '3.9,3.10'"); assert_true("python_version in '3.9 or 3.10'"); - // e.g, when one of the values cannot be true - // TODO(zanieb): This seems like a quirk of the `python_full_version` normalization, this - // should just act as though the patch version isn't present - assert_false("python_version in '3.9 3.10.0 3.11'"); + // This is an edge case that happens to be supported, but is not critical to support. + assert_simplifies( + "python_version in '3.9 3.10.0 3.11'", + "python_full_version >= '3.9' and python_full_version < '3.12'", + ); assert_simplifies("python_version == '3.9'", "python_full_version == '3.9.*'"); assert_simplifies( "python_version == '3.9.0'", "python_full_version == '3.9.*'", ); + assert_simplifies( + "python_version == '3.9.0.*'", + "python_full_version == '3.9.*'", + ); + assert_simplifies( + "python_version == '3.*'", + "python_full_version >= '3' and python_full_version < '4'", + ); // ` in` // e.g., when the range is not contiguous @@ -2528,6 +2537,68 @@ mod test { ); } + #[test] + fn test_python_version_equal_star() { + // Input, equivalent with python_version, equivalent with python_full_version + let cases = [ + ("3.*", "3.*", "3.*"), + ("3.0.*", "3.0", "3.0.*"), + ("3.0.0.*", "3.0", "3.0.*"), + ("3.9.*", "3.9", "3.9.*"), + ("3.9.0.*", "3.9", "3.9.*"), + ("3.9.0.0.*", "3.9", "3.9.*"), + ]; + for (input, equal_python_version, equal_python_full_version) in cases { + assert_eq!( + m(&format!("python_version == '{input}'")), + m(&format!("python_version == '{equal_python_version}'")), + "{input} {equal_python_version}" + ); + assert_eq!( + m(&format!("python_version == '{input}'")), + m(&format!( + "python_full_version == '{equal_python_full_version}'" + )), + "{input} {equal_python_full_version}" + ); + } + + let cases_false = ["3.9.1.*", "3.9.1.0.*", "3.9.1.0.0.*"]; + for input in cases_false { + assert!( + m(&format!("python_version == '{input}'")).is_false(), + "{input}" + ); + } + } + + #[test] + fn test_tilde_equal_normalization() { + assert_eq!( + m("python_version ~= '3.10.0'"), + m("python_version >= '3.10.0' and python_version < '3.11.0'") + ); + + // Two digit versions such as `python_version` get padded with a zero, so they can never + // match + assert_eq!(m("python_version ~= '3.10.1'"), MarkerTree::FALSE); + + assert_eq!( + m("python_version ~= '3.10'"), + m("python_version >= '3.10' and python_version < '4.0'") + ); + + assert_eq!( + m("python_full_version ~= '3.10.0'"), + m("python_full_version >= '3.10.0' and python_full_version < '3.11.0'") + ); + + assert_eq!( + m("python_full_version ~= '3.10'"), + m("python_full_version >= '3.10' and python_full_version < '4.0'") + ); + } + /// This tests marker implication. /// /// Specifically, these test cases come from a [bug] where `foo` and `bar` @@ -3324,4 +3395,32 @@ mod test { ] ); } + + /// Case a: There is no version `3` (no trailing zero) in the interner yet. + #[test] + fn marker_normalization_a() { + let left_tree = m("python_version == '3.0.*'"); + let left = left_tree.try_to_string().unwrap(); + let right = "python_full_version == '3.0.*'"; + assert_eq!(left, right, "{left} != {right}"); + } + + /// Case b: There is already a version `3` (no trailing zero) in the interner. + #[test] + fn marker_normalization_b() { + m("python_version >= '3' and python_version <= '3.0'"); + + let left_tree = m("python_version == '3.0.*'"); + let left = left_tree.try_to_string().unwrap(); + let right = "python_full_version == '3.0.*'"; + assert_eq!(left, right, "{left} != {right}"); + } + + #[test] + fn marker_normalization_c() { + let left_tree = MarkerTree::from_str("python_version == '3.10.0.*'").unwrap(); + let left = left_tree.try_to_string().unwrap(); + let right = "python_full_version == '3.10.*'"; + assert_eq!(left, right, "{left} != {right}"); + } } diff --git a/crates/uv-pypi-types/src/conflicts.rs b/crates/uv-pypi-types/src/conflicts.rs index dd1e96b77..81064955a 100644 --- a/crates/uv-pypi-types/src/conflicts.rs +++ b/crates/uv-pypi-types/src/conflicts.rs @@ -3,7 +3,9 @@ use petgraph::{ graph::{DiGraph, NodeIndex}, }; use rustc_hash::{FxHashMap, FxHashSet}; -use std::{borrow::Cow, collections::BTreeSet, hash::Hash, rc::Rc}; +#[cfg(feature = "schemars")] +use std::borrow::Cow; +use std::{collections::BTreeSet, hash::Hash, rc::Rc}; use uv_normalize::{ExtraName, GroupName, PackageName}; use crate::dependency_groups::{DependencyGroupSpecifier, DependencyGroups}; diff --git a/crates/uv-pypi-types/src/identifier.rs b/crates/uv-pypi-types/src/identifier.rs index 972a327ae..47439f2c9 100644 --- a/crates/uv-pypi-types/src/identifier.rs +++ b/crates/uv-pypi-types/src/identifier.rs @@ -1,4 +1,5 @@ use serde::{Serialize, Serializer}; +#[cfg(feature = "schemars")] use std::borrow::Cow; use std::fmt::Display; use std::str::FromStr; diff --git a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__line-endings-poetry-with-hashes.txt.snap b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__line-endings-poetry-with-hashes.txt.snap index ad5e2a0e6..e13ab75b7 100644 --- a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__line-endings-poetry-with-hashes.txt.snap +++ b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__line-endings-poetry-with-hashes.txt.snap @@ -1,6 +1,7 @@ --- source: crates/uv-requirements-txt/src/lib.rs expression: actual +snapshot_kind: text --- RequirementsTxt { requirements: [ @@ -23,7 +24,7 @@ RequirementsTxt { ), ), ), - marker: python_full_version >= '3.8' and python_full_version < '4.0', + marker: python_full_version >= '3.8' and python_full_version < '4', origin: Some( File( "/poetry-with-hashes.txt", @@ -54,7 +55,7 @@ RequirementsTxt { ), ), ), - marker: python_full_version >= '3.8' and python_full_version < '4.0', + marker: python_full_version >= '3.8' and python_full_version < '4', origin: Some( File( "/poetry-with-hashes.txt", @@ -85,7 +86,7 @@ RequirementsTxt { ), ), ), - marker: python_full_version >= '3.8' and python_full_version < '4.0' and sys_platform == 'win32', + marker: python_full_version >= '3.8' and python_full_version < '4' and sys_platform == 'win32', origin: Some( File( "/poetry-with-hashes.txt", @@ -116,7 +117,7 @@ RequirementsTxt { ), ), ), - marker: python_full_version >= '3.8' and python_full_version < '4.0', + marker: python_full_version >= '3.8' and python_full_version < '4', origin: Some( File( "/poetry-with-hashes.txt", @@ -148,7 +149,7 @@ RequirementsTxt { ), ), ), - marker: python_full_version >= '3.8' and python_full_version < '4.0', + marker: python_full_version >= '3.8' and python_full_version < '4', origin: Some( File( "/poetry-with-hashes.txt", diff --git a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-poetry-with-hashes.txt.snap b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-poetry-with-hashes.txt.snap index ad5e2a0e6..e13ab75b7 100644 --- a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-poetry-with-hashes.txt.snap +++ b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-poetry-with-hashes.txt.snap @@ -1,6 +1,7 @@ --- source: crates/uv-requirements-txt/src/lib.rs expression: actual +snapshot_kind: text --- RequirementsTxt { requirements: [ @@ -23,7 +24,7 @@ RequirementsTxt { ), ), ), - marker: python_full_version >= '3.8' and python_full_version < '4.0', + marker: python_full_version >= '3.8' and python_full_version < '4', origin: Some( File( "/poetry-with-hashes.txt", @@ -54,7 +55,7 @@ RequirementsTxt { ), ), ), - marker: python_full_version >= '3.8' and python_full_version < '4.0', + marker: python_full_version >= '3.8' and python_full_version < '4', origin: Some( File( "/poetry-with-hashes.txt", @@ -85,7 +86,7 @@ RequirementsTxt { ), ), ), - marker: python_full_version >= '3.8' and python_full_version < '4.0' and sys_platform == 'win32', + marker: python_full_version >= '3.8' and python_full_version < '4' and sys_platform == 'win32', origin: Some( File( "/poetry-with-hashes.txt", @@ -116,7 +117,7 @@ RequirementsTxt { ), ), ), - marker: python_full_version >= '3.8' and python_full_version < '4.0', + marker: python_full_version >= '3.8' and python_full_version < '4', origin: Some( File( "/poetry-with-hashes.txt", @@ -148,7 +149,7 @@ RequirementsTxt { ), ), ), - marker: python_full_version >= '3.8' and python_full_version < '4.0', + marker: python_full_version >= '3.8' and python_full_version < '4', origin: Some( File( "/poetry-with-hashes.txt", diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index ff5465042..1c375b2e3 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -5013,14 +5013,14 @@ fn lock_requires_python_not_equal() -> Result<()> { "#, )?; - uv_snapshot!(context.filters(), context.lock(), @r###" + uv_snapshot!(context.filters(), context.lock(), @r" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 2 packages in [TIME] - "###); + "); let lock = fs_err::read_to_string(&lockfile).unwrap(); @@ -27522,7 +27522,7 @@ fn windows_arm() -> Result<()> { lock, @r#" version = 1 revision = 2 - requires-python = ">=3.12.[X], <3.13" + requires-python = "==3.12.*" resolution-markers = [ "platform_machine == 'x86_64' and sys_platform == 'linux'", "platform_machine == 'AMD64' and sys_platform == 'win32'", @@ -27599,7 +27599,7 @@ fn windows_amd64_required() -> Result<()> { lock, @r#" version = 1 revision = 2 - requires-python = ">=3.12.[X], <3.13" + requires-python = "==3.12.*" required-markers = [ "platform_machine == 'x86' and sys_platform == 'win32'", "platform_machine == 'AMD64' and sys_platform == 'win32'", @@ -28725,3 +28725,34 @@ fn lock_prefix_match() -> Result<()> { Ok(()) } + +/// Regression test for . +#[test] +fn test_tilde_equals_python_version() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "debug" + version = "0.1.0" + requires-python = ">=3.9" + dependencies = [ + "anyio==4.2.0; python_full_version >= '3.11'", + "anyio==4.3.0; python_full_version ~= '3.10.0'", + ] + "#, + )?; + + uv_snapshot!(context.filters(), context.lock(), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 7 packages in [TIME] + "); + + Ok(()) +} From 9af3e9b6ec5fd8d84644d5ecaf45be273ed21a3a Mon Sep 17 00:00:00 2001 From: konsti Date: Tue, 1 Jul 2025 18:00:30 +0200 Subject: [PATCH 147/185] Remove unnecessary codspeed deps (#14396) See https://github.com/CodSpeedHQ/codspeed-rust/pull/108 --- Cargo.lock | 135 ++----------------------------------- crates/uv-bench/Cargo.toml | 2 +- 2 files changed, 7 insertions(+), 130 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0c5bab5a7..f961b2b5a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -690,9 +690,9 @@ checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "codspeed" -version = "3.0.1" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b41c7ae78309311a5ce5f31dbdae2d7d6ad68b057163eaa23e05b68c9ffac8" +checksum = "922018102595f6668cdd09c03f4bff2d951ce2318c6dca4fe11bdcb24b65b2bf" dependencies = [ "anyhow", "bincode", @@ -708,9 +708,9 @@ dependencies = [ [[package]] name = "codspeed-criterion-compat" -version = "3.0.1" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2c699a447da1c442a81e14f01363f44ffd68eb13fedfd3ed13ce5cf30f738d9" +checksum = "24d8ad82d2383cb74995f58993cbdd2914aed57b2f91f46580310dd81dc3d05a" dependencies = [ "codspeed", "codspeed-criterion-compat-walltime", @@ -719,9 +719,9 @@ dependencies = [ [[package]] name = "codspeed-criterion-compat-walltime" -version = "3.0.1" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c104831cdfa515c938d8ec5b8dd38030a5f815b113762f0d03f494177b261cfd" +checksum = "61badaa6c452d192a29f8387147888f0ab358553597c3fe9bf8a162ef7c2fa64" dependencies = [ "anes", "cast", @@ -2107,12 +2107,6 @@ version = "0.2.171" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" -[[package]] -name = "libm" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" - [[package]] name = "libmimalloc-sys" version = "0.1.39" @@ -2233,16 +2227,6 @@ dependencies = [ "regex-automata 0.1.10", ] -[[package]] -name = "matrixmultiply" -version = "0.3.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08" -dependencies = [ - "autocfg", - "rawpointer", -] - [[package]] name = "md-5" version = "0.10.6" @@ -2380,23 +2364,6 @@ dependencies = [ "syn", ] -[[package]] -name = "nalgebra" -version = "0.33.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26aecdf64b707efd1310e3544d709c5c0ac61c13756046aaaba41be5c4f66a3b" -dependencies = [ - "approx", - "matrixmultiply", - "num-complex", - "num-rational", - "num-traits", - "rand", - "rand_distr", - "simba", - "typenum", -] - [[package]] name = "nanoid" version = "0.4.0" @@ -2455,45 +2422,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-complex" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" -dependencies = [ - "num-bigint", - "num-integer", - "num-traits", -] - [[package]] name = "num-traits" version = "0.2.19" @@ -2501,7 +2429,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", - "libm", ] [[package]] @@ -3002,22 +2929,6 @@ dependencies = [ "getrandom 0.2.15", ] -[[package]] -name = "rand_distr" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" -dependencies = [ - "num-traits", - "rand", -] - -[[package]] -name = "rawpointer" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" - [[package]] name = "rayon" version = "1.10.0" @@ -3497,15 +3408,6 @@ version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" -[[package]] -name = "safe_arch" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b02de82ddbe1b636e6170c21be622223aea188ef2e139be0a5b219ec215323" -dependencies = [ - "bytemuck", -] - [[package]] name = "same-file" version = "1.0.6" @@ -3762,19 +3664,6 @@ dependencies = [ "libc", ] -[[package]] -name = "simba" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3a386a501cd104797982c15ae17aafe8b9261315b5d07e3ec803f2ea26be0fa" -dependencies = [ - "approx", - "num-complex", - "num-traits", - "paste", - "wide", -] - [[package]] name = "simd-adler32" version = "0.3.7" @@ -3861,9 +3750,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a3fe7c28c6512e766b0874335db33c94ad7b8f9054228ae1c2abd47ce7d335e" dependencies = [ "approx", - "nalgebra", "num-traits", - "rand", ] [[package]] @@ -6362,16 +6249,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "wide" -version = "0.7.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce5da8ecb62bcd8ec8b7ea19f69a51275e91299be594ea5cc6ef7819e16cd03" -dependencies = [ - "bytemuck", - "safe_arch", -] - [[package]] name = "widestring" version = "1.1.0" diff --git a/crates/uv-bench/Cargo.toml b/crates/uv-bench/Cargo.toml index afa73c32b..8c08d4dd2 100644 --- a/crates/uv-bench/Cargo.toml +++ b/crates/uv-bench/Cargo.toml @@ -42,7 +42,7 @@ uv-types = { workspace = true } uv-workspace = { workspace = true } anyhow = { workspace = true } -codspeed-criterion-compat = { version = "3.0.1", default-features = false, optional = true } +codspeed-criterion-compat = { version = "3.0.2", default-features = false, optional = true } criterion = { version = "0.6.0", default-features = false, features = [ "async_tokio", ] } From c777491bf4f829b4431cc3fa3513e57d2bfe0f5c Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Tue, 1 Jul 2025 11:29:10 -0500 Subject: [PATCH 148/185] Use the insiders requirements when building docs in CI (#14379) --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6c6d47ce2..701699239 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -443,7 +443,7 @@ jobs: - name: "Build docs (insiders)" if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }} - run: uvx --with-requirements docs/requirements.txt mkdocs build --strict -f mkdocs.insiders.yml + run: uvx --with-requirements docs/requirements-insiders.txt mkdocs build --strict -f mkdocs.insiders.yml build-binary-linux-libc: timeout-minutes: 10 From c0786832170ec0eac636d19a40a2b1ad7d82f389 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 1 Jul 2025 12:50:19 -0400 Subject: [PATCH 149/185] Only drop build directories on program exit (#14304) ## Summary This PR ensures that we avoid cleaning up build directories until the end of a resolve-and-install cycle. It's not bulletproof (since we could still run into issues with `uv lock` followed by `uv sync` whereby a build directory gets cleaned up that's still referenced in the `build` artifacts), but it at least gets PyTorch building without error with `uv pip install .`, which is a case that's been reported several times. Closes https://github.com/astral-sh/uv/issues/14269. --- Cargo.lock | 2 ++ crates/uv-build-frontend/src/lib.rs | 4 ++++ crates/uv-dispatch/src/lib.rs | 22 +++++++++++++++++----- crates/uv-distribution/src/source/mod.rs | 24 +++++++++++++++++++----- crates/uv-types/Cargo.toml | 2 ++ crates/uv-types/src/builds.rs | 13 +++++++++++++ crates/uv-types/src/traits.rs | 8 ++++++++ 7 files changed, 65 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f961b2b5a..fa75564dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5937,7 +5937,9 @@ name = "uv-types" version = "0.0.1" dependencies = [ "anyhow", + "boxcar", "rustc-hash", + "tempfile", "thiserror 2.0.12", "uv-cache", "uv-configuration", diff --git a/crates/uv-build-frontend/src/lib.rs b/crates/uv-build-frontend/src/lib.rs index df6fc09cf..d35526951 100644 --- a/crates/uv-build-frontend/src/lib.rs +++ b/crates/uv-build-frontend/src/lib.rs @@ -862,6 +862,10 @@ impl SourceBuild { } impl SourceBuildTrait for SourceBuild { + fn into_build_dir(self) -> TempDir { + self.temp_dir + } + async fn metadata(&mut self) -> Result, AnyErrorBuild> { Ok(self.get_metadata_without_build().await?) } diff --git a/crates/uv-dispatch/src/lib.rs b/crates/uv-dispatch/src/lib.rs index 207f241ad..d95a4f39a 100644 --- a/crates/uv-dispatch/src/lib.rs +++ b/crates/uv-dispatch/src/lib.rs @@ -2,15 +2,15 @@ //! [installer][`uv_installer`] and [build][`uv_build`] through [`BuildDispatch`] //! implementing [`BuildContext`]. -use std::ffi::{OsStr, OsString}; -use std::path::Path; - use anyhow::{Context, Result}; use futures::FutureExt; use itertools::Itertools; use rustc_hash::FxHashMap; +use std::ffi::{OsStr, OsString}; +use std::path::Path; use thiserror::Error; use tracing::{debug, instrument, trace}; + use uv_build_backend::check_direct_build; use uv_build_frontend::{SourceBuild, SourceBuildContext}; use uv_cache::Cache; @@ -35,8 +35,8 @@ use uv_resolver::{ PythonRequirement, Resolver, ResolverEnvironment, }; use uv_types::{ - AnyErrorBuild, BuildContext, BuildIsolation, BuildStack, EmptyInstalledPackages, HashStrategy, - InFlight, + AnyErrorBuild, BuildArena, BuildContext, BuildIsolation, BuildStack, EmptyInstalledPackages, + HashStrategy, InFlight, }; use uv_workspace::WorkspaceCache; @@ -179,6 +179,10 @@ impl BuildContext for BuildDispatch<'_> { &self.shared_state.git } + fn build_arena(&self) -> &BuildArena { + &self.shared_state.build_arena + } + fn capabilities(&self) -> &IndexCapabilities { &self.shared_state.capabilities } @@ -521,6 +525,8 @@ pub struct SharedState { index: InMemoryIndex, /// The downloaded distributions. in_flight: InFlight, + /// Build directories for any PEP 517 builds executed during resolution or installation. + build_arena: BuildArena, } impl SharedState { @@ -533,6 +539,7 @@ impl SharedState { Self { git: self.git.clone(), capabilities: self.capabilities.clone(), + build_arena: self.build_arena.clone(), ..Default::default() } } @@ -556,4 +563,9 @@ impl SharedState { pub fn capabilities(&self) -> &IndexCapabilities { &self.capabilities } + + /// Return the [`BuildArena`] used by the [`SharedState`]. + pub fn build_arena(&self) -> &BuildArena { + &self.build_arena + } } diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs index 7be26fece..5116ce72b 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -2276,6 +2276,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { fs::create_dir_all(&cache_shard) .await .map_err(Error::CacheWrite)?; + // Try a direct build if that isn't disabled and the uv build backend is used. let disk_filename = if let Some(name) = self .build_context @@ -2296,7 +2297,8 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { // In the uv build backend, the normalized filename and the disk filename are the same. name.to_string() } else { - self.build_context + let builder = self + .build_context .setup_build( source_root, subdirectory, @@ -2313,10 +2315,17 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { self.build_stack.cloned().unwrap_or_default(), ) .await - .map_err(|err| Error::Build(err.into()))? - .wheel(temp_dir.path()) - .await - .map_err(Error::Build)? + .map_err(|err| Error::Build(err.into()))?; + + // Build the wheel. + let wheel = builder.wheel(temp_dir.path()).await.map_err(Error::Build)?; + + // Store a reference to the build context. + self.build_context + .build_arena() + .push(builder.into_build_dir()); + + wheel }; // Read the metadata from the wheel. @@ -2398,6 +2407,11 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { return Ok(None); }; + // Store a reference to the build context. + self.build_context + .build_arena() + .push(builder.into_build_dir()); + // Read the metadata from disk. debug!("Prepared metadata for: {source}"); let content = fs::read(dist_info.join("METADATA")) diff --git a/crates/uv-types/Cargo.toml b/crates/uv-types/Cargo.toml index 0973a5218..7c1ca60b3 100644 --- a/crates/uv-types/Cargo.toml +++ b/crates/uv-types/Cargo.toml @@ -31,7 +31,9 @@ uv-redacted = { workspace = true } uv-workspace = { workspace = true } anyhow = { workspace = true } +boxcar = { workspace = true } rustc-hash = { workspace = true } +tempfile = { workspace = true } thiserror = { workspace = true } [features] diff --git a/crates/uv-types/src/builds.rs b/crates/uv-types/src/builds.rs index ea5e0b6a3..759dd4135 100644 --- a/crates/uv-types/src/builds.rs +++ b/crates/uv-types/src/builds.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; +use tempfile::TempDir; use uv_pep508::PackageName; use uv_python::PythonEnvironment; @@ -37,3 +39,14 @@ impl BuildIsolation<'_> { } } } + +/// An arena of temporary directories used for builds. +#[derive(Default, Debug, Clone)] +pub struct BuildArena(Arc>); + +impl BuildArena { + /// Push a new temporary directory into the arena. + pub fn push(&self, temp_dir: TempDir) { + self.0.push(temp_dir); + } +} diff --git a/crates/uv-types/src/traits.rs b/crates/uv-types/src/traits.rs index 6f724b27a..16f58fe13 100644 --- a/crates/uv-types/src/traits.rs +++ b/crates/uv-types/src/traits.rs @@ -5,7 +5,9 @@ use std::path::{Path, PathBuf}; use anyhow::Result; use rustc_hash::FxHashSet; +use tempfile::TempDir; +use crate::BuildArena; use uv_cache::Cache; use uv_configuration::{BuildKind, BuildOptions, BuildOutput, ConfigSettings, SourceStrategy}; use uv_distribution_filename::DistFilename; @@ -67,6 +69,9 @@ pub trait BuildContext { /// Return a reference to the Git resolver. fn git(&self) -> &GitResolver; + /// Return a reference to the build arena. + fn build_arena(&self) -> &BuildArena; + /// Return a reference to the discovered registry capabilities. fn capabilities(&self) -> &IndexCapabilities; @@ -148,6 +153,9 @@ pub trait BuildContext { /// You can either call only `wheel()` to build the wheel directly, call only `metadata()` to get /// the metadata without performing the actual or first call `metadata()` and then `wheel()`. pub trait SourceBuildTrait { + /// Return the temporary build directory. + fn into_build_dir(self) -> TempDir; + /// A wrapper for `uv_build::SourceBuild::get_metadata_without_build`. /// /// For PEP 517 builds, this calls `prepare_metadata_for_build_wheel` From 85358fe9c615a9f75b1e7367018c22b28d540ecf Mon Sep 17 00:00:00 2001 From: Jack O'Connor Date: Mon, 30 Jun 2025 13:37:51 -0700 Subject: [PATCH 150/185] Keep track of retries in `ManagedPythonDownload::fetch_with_retry` If/when we see https://github.com/astral-sh/uv/issues/14171 again, this should clarify whether our retry logic was skipped (i.e. a transient error wasn't correctly identified as transient), or whether we exhausted our retries. Previously, if you ran a local example fileserver as in https://github.com/astral-sh/uv/issues/14171#issuecomment-3014580701 and then you tried to install Python from it, you'd get: ``` $ export UV_TEST_NO_CLI_PROGRESS=1 $ uv python install 3.8.20 --mirror http://localhost:8000 2>&1 | cat error: Failed to install cpython-3.8.20-linux-x86_64-gnu Caused by: Failed to extract archive: cpython-3.8.20-20241002-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz Caused by: failed to unpack `/home/jacko/.local/share/uv/python/.temp/.tmpS4sHHZ/python/lib/libpython3.8.so.1.0` Caused by: failed to unpack `python/lib/libpython3.8.so.1.0` into `/home/jacko/.local/share/uv/python/.temp/.tmpS4sHHZ/python/lib/libpython3.8.so.1.0` Caused by: error decoding response body Caused by: request or response body error Caused by: error reading a body from connection Caused by: Connection reset by peer (os error 104) ``` With this change you get: ``` error: Failed to install cpython-3.8.20-linux-x86_64-gnu Caused by: Request failed after 3 retries Caused by: Failed to extract archive: cpython-3.8.20-20241002-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz Caused by: failed to unpack `/home/jacko/.local/share/uv/python/.temp/.tmp4Ia24w/python/lib/libpython3.8.so.1.0` Caused by: failed to unpack `python/lib/libpython3.8.so.1.0` into `/home/jacko/.local/share/uv/python/.temp/.tmp4Ia24w/python/lib/libpython3.8.so.1.0` Caused by: error decoding response body Caused by: request or response body error Caused by: error reading a body from connection Caused by: Connection reset by peer (os error 104) ``` At the same time, I'm updating the way we handle the retry count to avoid nested retry loops exceeding the intended number of attempts, as I mentioned at https://github.com/astral-sh/uv/issues/14069#issuecomment-3020634281. It's not clear to me whether we actually want this part of the change, and I need feedback here. --- crates/uv-python/src/downloads.rs | 84 +++++++++++++++++++++++-------- 1 file changed, 64 insertions(+), 20 deletions(-) diff --git a/crates/uv-python/src/downloads.rs b/crates/uv-python/src/downloads.rs index 2b0a7e669..ad516d096 100644 --- a/crates/uv-python/src/downloads.rs +++ b/crates/uv-python/src/downloads.rs @@ -12,7 +12,7 @@ use futures::TryStreamExt; use itertools::Itertools; use once_cell::sync::OnceCell; use owo_colors::OwoColorize; -use reqwest_retry::RetryPolicy; +use reqwest_retry::{RetryError, RetryPolicy}; use serde::Deserialize; use thiserror::Error; use tokio::io::{AsyncRead, AsyncWriteExt, BufWriter, ReadBuf}; @@ -111,6 +111,33 @@ pub enum Error { }, } +impl Error { + // Return the number of attempts that were made to complete this request before this error was + // returned. Note that e.g. 3 retries equates to 4 attempts. + // + // It's easier to do arithmetic with "attempts" instead of "retries", because if you have + // nested retry loops you can just add up all the attempts directly, while adding up the + // retries requires +1/-1 adjustments. + fn attempts(&self) -> u32 { + // Unfortunately different variants of `Error` track retry counts in different ways. We + // could consider unifying the variants we handle here in `Error::from_reqwest_middleware` + // instead, but both approaches will be fragile as new variants get added over time. + if let Error::NetworkErrorWithRetries { retries, .. } = self { + return retries + 1; + } + // TODO(jack): let-chains are stable as of Rust 1.88. We should use them here as soon as + // our rust-version is high enough. + if let Error::NetworkMiddlewareError(_, anyhow_error) = self { + if let Some(RetryError::WithRetries { retries, .. }) = + anyhow_error.downcast_ref::() + { + return retries + 1; + } + } + 1 + } +} + #[derive(Debug, PartialEq, Eq, Clone, Hash)] pub struct ManagedPythonDownload { key: PythonInstallationKey, @@ -695,7 +722,8 @@ impl ManagedPythonDownload { pypy_install_mirror: Option<&str>, reporter: Option<&dyn Reporter>, ) -> Result { - let mut n_past_retries = 0; + let mut total_attempts = 0; + let mut retried_here = false; let start_time = SystemTime::now(); let retry_policy = client.retry_policy(); loop { @@ -710,25 +738,41 @@ impl ManagedPythonDownload { reporter, ) .await; - if result - .as_ref() - .err() - .is_some_and(|err| is_extended_transient_error(err)) - { - let retry_decision = retry_policy.should_retry(start_time, n_past_retries); - if let reqwest_retry::RetryDecision::Retry { execute_after } = retry_decision { - debug!( - "Transient failure while handling response for {}; retrying...", - self.key() - ); - let duration = execute_after - .duration_since(SystemTime::now()) - .unwrap_or_else(|_| Duration::default()); - tokio::time::sleep(duration).await; - n_past_retries += 1; - continue; + let result = match result { + Ok(download_result) => Ok(download_result), + Err(err) => { + // Inner retry loops (e.g. `reqwest-retry` middleware) might make more than one + // attempt per error we see here. + total_attempts += err.attempts(); + // We currently interpret e.g. "3 retries" to mean we should make 4 attempts. + let n_past_retries = total_attempts - 1; + if is_extended_transient_error(&err) { + let retry_decision = retry_policy.should_retry(start_time, n_past_retries); + if let reqwest_retry::RetryDecision::Retry { execute_after } = + retry_decision + { + debug!( + "Transient failure while handling response for {}; retrying...", + self.key() + ); + let duration = execute_after + .duration_since(SystemTime::now()) + .unwrap_or_else(|_| Duration::default()); + tokio::time::sleep(duration).await; + retried_here = true; + continue; // Retry. + } + } + if retried_here { + Err(Error::NetworkErrorWithRetries { + err: Box::new(err), + retries: n_past_retries, + }) + } else { + Err(err) + } } - } + }; return result; } } From d9f9ed4aec8b0a68b66c0346450aa23744dc69ef Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 1 Jul 2025 13:15:47 -0400 Subject: [PATCH 151/185] Reuse build (virtual) environments across resolution and installation (#14338) ## Summary The basic idea here is that we can (should) reuse a build environment across resolution (`prepare_metadata_for_build_wheel`) and installation. This also happens to solve the build-PyTorch-from-source problem, since we use a consistent build environment between the invocations. Since `SourceDistributionBuilder` is stateless, we instead store the builds on `BuildContext`, and we key them by various properties: the underlying interpreter, the configuration settings, etc. This just ensures that if we build the same package twice within a process, we don't accidentally reuse an incompatible build (virtual) environment. (Note that still drop build environments at the end of the command, and don't attempt to reuse them across processes.) Closes #14269. --- Cargo.lock | 3 +- crates/uv-build-frontend/src/lib.rs | 51 +++---- crates/uv-configuration/src/build_options.rs | 2 +- crates/uv-configuration/src/sources.rs | 4 +- crates/uv-dispatch/src/lib.rs | 11 +- crates/uv-distribution/src/error.rs | 2 + crates/uv-distribution/src/source/mod.rs | 138 ++++++++++++++----- crates/uv-types/Cargo.toml | 3 +- crates/uv-types/src/builds.rs | 48 +++++-- crates/uv-types/src/traits.rs | 17 +-- 10 files changed, 189 insertions(+), 90 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fa75564dc..802b069f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5937,9 +5937,8 @@ name = "uv-types" version = "0.0.1" dependencies = [ "anyhow", - "boxcar", + "dashmap", "rustc-hash", - "tempfile", "thiserror 2.0.12", "uv-cache", "uv-configuration", diff --git a/crates/uv-build-frontend/src/lib.rs b/crates/uv-build-frontend/src/lib.rs index d35526951..06c07425c 100644 --- a/crates/uv-build-frontend/src/lib.rs +++ b/crates/uv-build-frontend/src/lib.rs @@ -259,8 +259,6 @@ pub struct SourceBuild { environment_variables: FxHashMap, /// Runner for Python scripts. runner: PythonRunner, - /// A file lock representing the source tree, currently only used with setuptools. - _source_tree_lock: Option, } impl SourceBuild { @@ -394,23 +392,6 @@ impl SourceBuild { OsString::from(venv.scripts()) }; - // Depending on the command, setuptools puts `*.egg-info`, `build/`, and `dist/` in the - // source tree, and concurrent invocations of setuptools using the same source dir can - // stomp on each other. We need to lock something to fix that, but we don't want to dump a - // `.lock` file into the source tree that the user will need to .gitignore. Take a global - // proxy lock instead. - let mut source_tree_lock = None; - if pep517_backend.is_setuptools() { - debug!("Locking the source tree for setuptools"); - let canonical_source_path = source_tree.canonicalize()?; - let lock_path = std::env::temp_dir().join(format!( - "uv-setuptools-{}.lock", - cache_digest(&canonical_source_path) - )); - source_tree_lock = - Some(LockedFile::acquire(lock_path, source_tree.to_string_lossy()).await?); - } - // Create the PEP 517 build environment. If build isolation is disabled, we assume the build // environment is already setup. let runner = PythonRunner::new(concurrent_builds, level); @@ -457,10 +438,30 @@ impl SourceBuild { environment_variables, modified_path, runner, - _source_tree_lock: source_tree_lock, }) } + /// Acquire a lock on the source tree, if necessary. + async fn acquire_lock(&self) -> Result, Error> { + // Depending on the command, setuptools puts `*.egg-info`, `build/`, and `dist/` in the + // source tree, and concurrent invocations of setuptools using the same source dir can + // stomp on each other. We need to lock something to fix that, but we don't want to dump a + // `.lock` file into the source tree that the user will need to .gitignore. Take a global + // proxy lock instead. + let mut source_tree_lock = None; + if self.pep517_backend.is_setuptools() { + debug!("Locking the source tree for setuptools"); + let canonical_source_path = self.source_tree.canonicalize()?; + let lock_path = env::temp_dir().join(format!( + "uv-setuptools-{}.lock", + cache_digest(&canonical_source_path) + )); + source_tree_lock = + Some(LockedFile::acquire(lock_path, self.source_tree.to_string_lossy()).await?); + } + Ok(source_tree_lock) + } + async fn get_resolved_requirements( build_context: &impl BuildContext, source_build_context: SourceBuildContext, @@ -631,6 +632,9 @@ impl SourceBuild { return Ok(Some(metadata_dir.clone())); } + // Lock the source tree, if necessary. + let _lock = self.acquire_lock().await?; + // Hatch allows for highly dynamic customization of metadata via hooks. In such cases, Hatch // can't uphold the PEP 517 contract, in that the metadata Hatch would return by // `prepare_metadata_for_build_wheel` isn't guaranteed to match that of the built wheel. @@ -749,6 +753,9 @@ impl SourceBuild { /// Perform a PEP 517 build for a wheel or source distribution (sdist). async fn pep517_build(&self, output_dir: &Path) -> Result { + // Lock the source tree, if necessary. + let _lock = self.acquire_lock().await?; + // Write the hook output to a file so that we can read it back reliably. let outfile = self .temp_dir @@ -862,10 +869,6 @@ impl SourceBuild { } impl SourceBuildTrait for SourceBuild { - fn into_build_dir(self) -> TempDir { - self.temp_dir - } - async fn metadata(&mut self) -> Result, AnyErrorBuild> { Ok(self.get_metadata_without_build().await?) } diff --git a/crates/uv-configuration/src/build_options.rs b/crates/uv-configuration/src/build_options.rs index 1a62a1a12..8b493cbf0 100644 --- a/crates/uv-configuration/src/build_options.rs +++ b/crates/uv-configuration/src/build_options.rs @@ -4,7 +4,7 @@ use uv_pep508::PackageName; use crate::{PackageNameSpecifier, PackageNameSpecifiers}; -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)] pub enum BuildKind { /// A PEP 517 wheel build. #[default] diff --git a/crates/uv-configuration/src/sources.rs b/crates/uv-configuration/src/sources.rs index c60d69ef4..f8d0c3367 100644 --- a/crates/uv-configuration/src/sources.rs +++ b/crates/uv-configuration/src/sources.rs @@ -1,4 +1,6 @@ -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[derive( + Debug, Default, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, +)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub enum SourceStrategy { /// Use `tool.uv.sources` when resolving dependencies. diff --git a/crates/uv-dispatch/src/lib.rs b/crates/uv-dispatch/src/lib.rs index d95a4f39a..222450539 100644 --- a/crates/uv-dispatch/src/lib.rs +++ b/crates/uv-dispatch/src/lib.rs @@ -2,12 +2,13 @@ //! [installer][`uv_installer`] and [build][`uv_build`] through [`BuildDispatch`] //! implementing [`BuildContext`]. +use std::ffi::{OsStr, OsString}; +use std::path::Path; + use anyhow::{Context, Result}; use futures::FutureExt; use itertools::Itertools; use rustc_hash::FxHashMap; -use std::ffi::{OsStr, OsString}; -use std::path::Path; use thiserror::Error; use tracing::{debug, instrument, trace}; @@ -179,7 +180,7 @@ impl BuildContext for BuildDispatch<'_> { &self.shared_state.git } - fn build_arena(&self) -> &BuildArena { + fn build_arena(&self) -> &BuildArena { &self.shared_state.build_arena } @@ -526,7 +527,7 @@ pub struct SharedState { /// The downloaded distributions. in_flight: InFlight, /// Build directories for any PEP 517 builds executed during resolution or installation. - build_arena: BuildArena, + build_arena: BuildArena, } impl SharedState { @@ -565,7 +566,7 @@ impl SharedState { } /// Return the [`BuildArena`] used by the [`SharedState`]. - pub fn build_arena(&self) -> &BuildArena { + pub fn build_arena(&self) -> &BuildArena { &self.build_arena } } diff --git a/crates/uv-distribution/src/error.rs b/crates/uv-distribution/src/error.rs index c19867e75..7c2a0f804 100644 --- a/crates/uv-distribution/src/error.rs +++ b/crates/uv-distribution/src/error.rs @@ -108,6 +108,8 @@ pub enum Error { CacheHeal(String, HashAlgorithm), #[error("The source distribution requires Python {0}, but {1} is installed")] RequiresPython(VersionSpecifiers, Version), + #[error("Failed to identify base Python interpreter")] + BaseInterpreter(#[source] std::io::Error), /// A generic request middleware error happened while making a request. /// Refer to the error message for more details. diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs index 5116ce72b..2b73eb4ff 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -43,7 +43,7 @@ use uv_normalize::PackageName; use uv_pep440::{Version, release_specifiers_to_ranges}; use uv_platform_tags::Tags; use uv_pypi_types::{HashAlgorithm, HashDigest, HashDigests, PyProjectToml, ResolutionMetadata}; -use uv_types::{BuildContext, BuildStack, SourceBuildTrait}; +use uv_types::{BuildContext, BuildKey, BuildStack, SourceBuildTrait}; use uv_workspace::pyproject::ToolUvSources; use crate::distribution_database::ManagedClient; @@ -2297,35 +2297,73 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { // In the uv build backend, the normalized filename and the disk filename are the same. name.to_string() } else { - let builder = self - .build_context - .setup_build( - source_root, - subdirectory, - source_root, - Some(&source.to_string()), - source.as_dist(), - source_strategy, - if source.is_editable() { - BuildKind::Editable - } else { - BuildKind::Wheel - }, - BuildOutput::Debug, - self.build_stack.cloned().unwrap_or_default(), - ) - .await - .map_err(|err| Error::Build(err.into()))?; + // Identify the base Python interpreter to use in the cache key. + let base_python = if cfg!(unix) { + self.build_context + .interpreter() + .find_base_python() + .map_err(Error::BaseInterpreter)? + } else { + self.build_context + .interpreter() + .to_base_python() + .map_err(Error::BaseInterpreter)? + }; - // Build the wheel. - let wheel = builder.wheel(temp_dir.path()).await.map_err(Error::Build)?; + let build_kind = if source.is_editable() { + BuildKind::Editable + } else { + BuildKind::Wheel + }; - // Store a reference to the build context. - self.build_context - .build_arena() - .push(builder.into_build_dir()); + let build_key = BuildKey { + base_python: base_python.into_boxed_path(), + source_root: source_root.to_path_buf().into_boxed_path(), + subdirectory: subdirectory + .map(|subdirectory| subdirectory.to_path_buf().into_boxed_path()), + source_strategy, + build_kind, + }; - wheel + if let Some(builder) = self.build_context.build_arena().remove(&build_key) { + debug!("Creating build environment for: {source}"); + let wheel = builder.wheel(temp_dir.path()).await.map_err(Error::Build)?; + + // Store the build context. + self.build_context.build_arena().insert(build_key, builder); + + wheel + } else { + debug!("Reusing existing build environment for: {source}"); + + let builder = self + .build_context + .setup_build( + source_root, + subdirectory, + source_root, + Some(&source.to_string()), + source.as_dist(), + source_strategy, + if source.is_editable() { + BuildKind::Editable + } else { + BuildKind::Wheel + }, + BuildOutput::Debug, + self.build_stack.cloned().unwrap_or_default(), + ) + .await + .map_err(|err| Error::Build(err.into()))?; + + // Build the wheel. + let wheel = builder.wheel(temp_dir.path()).await.map_err(Error::Build)?; + + // Store the build context. + self.build_context.build_arena().insert(build_key, builder); + + wheel + } }; // Read the metadata from the wheel. @@ -2380,6 +2418,26 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { } } + // Identify the base Python interpreter to use in the cache key. + let base_python = if cfg!(unix) { + self.build_context + .interpreter() + .find_base_python() + .map_err(Error::BaseInterpreter)? + } else { + self.build_context + .interpreter() + .to_base_python() + .map_err(Error::BaseInterpreter)? + }; + + // Determine whether this is an editable or non-editable build. + let build_kind = if source.is_editable() { + BuildKind::Editable + } else { + BuildKind::Wheel + }; + // Set up the builder. let mut builder = self .build_context @@ -2390,11 +2448,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { Some(&source.to_string()), source.as_dist(), source_strategy, - if source.is_editable() { - BuildKind::Editable - } else { - BuildKind::Wheel - }, + build_kind, BuildOutput::Debug, self.build_stack.cloned().unwrap_or_default(), ) @@ -2403,15 +2457,25 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { // Build the metadata. let dist_info = builder.metadata().await.map_err(Error::Build)?; + + // Store the build context. + self.build_context.build_arena().insert( + BuildKey { + base_python: base_python.into_boxed_path(), + source_root: source_root.to_path_buf().into_boxed_path(), + subdirectory: subdirectory + .map(|subdirectory| subdirectory.to_path_buf().into_boxed_path()), + source_strategy, + build_kind, + }, + builder, + ); + + // Return the `.dist-info` directory, if it exists. let Some(dist_info) = dist_info else { return Ok(None); }; - // Store a reference to the build context. - self.build_context - .build_arena() - .push(builder.into_build_dir()); - // Read the metadata from disk. debug!("Prepared metadata for: {source}"); let content = fs::read(dist_info.join("METADATA")) diff --git a/crates/uv-types/Cargo.toml b/crates/uv-types/Cargo.toml index 7c1ca60b3..f29af4ca4 100644 --- a/crates/uv-types/Cargo.toml +++ b/crates/uv-types/Cargo.toml @@ -31,9 +31,8 @@ uv-redacted = { workspace = true } uv-workspace = { workspace = true } anyhow = { workspace = true } -boxcar = { workspace = true } +dashmap = { workspace = true } rustc-hash = { workspace = true } -tempfile = { workspace = true } thiserror = { workspace = true } [features] diff --git a/crates/uv-types/src/builds.rs b/crates/uv-types/src/builds.rs index 759dd4135..e8c622057 100644 --- a/crates/uv-types/src/builds.rs +++ b/crates/uv-types/src/builds.rs @@ -1,5 +1,9 @@ +use std::path::Path; use std::sync::Arc; -use tempfile::TempDir; + +use dashmap::DashMap; + +use uv_configuration::{BuildKind, SourceStrategy}; use uv_pep508::PackageName; use uv_python::PythonEnvironment; @@ -40,13 +44,41 @@ impl BuildIsolation<'_> { } } -/// An arena of temporary directories used for builds. -#[derive(Default, Debug, Clone)] -pub struct BuildArena(Arc>); +/// A key for the build cache, which includes the interpreter, source root, subdirectory, source +/// strategy, and build kind. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct BuildKey { + pub base_python: Box, + pub source_root: Box, + pub subdirectory: Option>, + pub source_strategy: SourceStrategy, + pub build_kind: BuildKind, +} -impl BuildArena { - /// Push a new temporary directory into the arena. - pub fn push(&self, temp_dir: TempDir) { - self.0.push(temp_dir); +/// An arena of in-process builds. +#[derive(Debug)] +pub struct BuildArena(Arc>); + +impl Default for BuildArena { + fn default() -> Self { + Self(Arc::new(DashMap::new())) + } +} + +impl Clone for BuildArena { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + +impl BuildArena { + /// Insert a build entry into the arena. + pub fn insert(&self, key: BuildKey, value: T) { + self.0.insert(key, value); + } + + /// Remove a build entry from the arena. + pub fn remove(&self, key: &BuildKey) -> Option { + self.0.remove(key).map(|entry| entry.1) } } diff --git a/crates/uv-types/src/traits.rs b/crates/uv-types/src/traits.rs index 16f58fe13..a95367fef 100644 --- a/crates/uv-types/src/traits.rs +++ b/crates/uv-types/src/traits.rs @@ -5,9 +5,7 @@ use std::path::{Path, PathBuf}; use anyhow::Result; use rustc_hash::FxHashSet; -use tempfile::TempDir; -use crate::BuildArena; use uv_cache::Cache; use uv_configuration::{BuildKind, BuildOptions, BuildOutput, ConfigSettings, SourceStrategy}; use uv_distribution_filename::DistFilename; @@ -20,6 +18,8 @@ use uv_pep508::PackageName; use uv_python::{Interpreter, PythonEnvironment}; use uv_workspace::WorkspaceCache; +use crate::BuildArena; + /// Avoids cyclic crate dependencies between resolver, installer and builder. /// /// To resolve the dependencies of a packages, we may need to build one or more source @@ -70,7 +70,7 @@ pub trait BuildContext { fn git(&self) -> &GitResolver; /// Return a reference to the build arena. - fn build_arena(&self) -> &BuildArena; + fn build_arena(&self) -> &BuildArena; /// Return a reference to the discovered registry capabilities. fn capabilities(&self) -> &IndexCapabilities; @@ -153,9 +153,6 @@ pub trait BuildContext { /// You can either call only `wheel()` to build the wheel directly, call only `metadata()` to get /// the metadata without performing the actual or first call `metadata()` and then `wheel()`. pub trait SourceBuildTrait { - /// Return the temporary build directory. - fn into_build_dir(self) -> TempDir; - /// A wrapper for `uv_build::SourceBuild::get_metadata_without_build`. /// /// For PEP 517 builds, this calls `prepare_metadata_for_build_wheel` @@ -188,13 +185,13 @@ pub trait InstalledPackagesProvider: Clone + Send + Sync + 'static { pub struct EmptyInstalledPackages; impl InstalledPackagesProvider for EmptyInstalledPackages { - fn get_packages(&self, _name: &PackageName) -> Vec<&InstalledDist> { - Vec::new() - } - fn iter(&self) -> impl Iterator { std::iter::empty() } + + fn get_packages(&self, _name: &PackageName) -> Vec<&InstalledDist> { + Vec::new() + } } /// [`anyhow::Error`]-like wrapper type for [`BuildDispatch`] method return values, that also makes From 29fcd6faeeb3a2f2e69655d6d34f30b26c71c850 Mon Sep 17 00:00:00 2001 From: John Mumm Date: Tue, 1 Jul 2025 20:39:17 +0200 Subject: [PATCH 152/185] Fix test cases to match Cow variants (#14390) Updates `without_trailing_slash` and `without_fragment` to separately match values against `Cow` variants. Closes #14350 --- crates/uv-distribution-types/src/file.rs | 27 +++++++++++++++--------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/crates/uv-distribution-types/src/file.rs b/crates/uv-distribution-types/src/file.rs index 81e3e2878..a75af3977 100644 --- a/crates/uv-distribution-types/src/file.rs +++ b/crates/uv-distribution-types/src/file.rs @@ -272,42 +272,49 @@ mod tests { fn without_fragment() { // Borrows a URL without a fragment let url = UrlString("https://example.com/path".into()); - assert_eq!(url.without_fragment(), Cow::Borrowed(&url)); + assert_eq!(&*url.without_fragment(), &url); + assert!(matches!(url.without_fragment(), Cow::Borrowed(_))); // Removes the fragment if present on the URL let url = UrlString("https://example.com/path?query#fragment".into()); assert_eq!( - url.without_fragment(), - Cow::Owned(UrlString("https://example.com/path?query".into())) + &*url.without_fragment(), + &UrlString("https://example.com/path?query".into()) ); + assert!(matches!(url.without_fragment(), Cow::Owned(_))); } #[test] fn without_trailing_slash() { // Borrows a URL without a slash let url = UrlString("https://example.com/path".into()); - assert_eq!(url.without_trailing_slash(), Cow::Borrowed(&url)); + assert_eq!(&*url.without_trailing_slash(), &url); + assert!(matches!(url.without_trailing_slash(), Cow::Borrowed(_))); // Removes the trailing slash if present on the URL let url = UrlString("https://example.com/path/".into()); assert_eq!( - url.without_trailing_slash(), - Cow::Owned(UrlString("https://example.com/path".into())) + &*url.without_trailing_slash(), + &UrlString("https://example.com/path".into()) ); + assert!(matches!(url.without_trailing_slash(), Cow::Owned(_))); // Does not remove a trailing slash if it's the only path segment let url = UrlString("https://example.com/".into()); - assert_eq!(url.without_trailing_slash(), Cow::Borrowed(&url)); + assert_eq!(&*url.without_trailing_slash(), &url); + assert!(matches!(url.without_trailing_slash(), Cow::Borrowed(_))); // Does not remove a trailing slash if it's the only path segment with a missing scheme let url = UrlString("example.com/".into()); - assert_eq!(url.without_trailing_slash(), Cow::Borrowed(&url)); + assert_eq!(&*url.without_trailing_slash(), &url); + assert!(matches!(url.without_trailing_slash(), Cow::Borrowed(_))); // Removes the trailing slash when the scheme is missing let url = UrlString("example.com/path/".into()); assert_eq!( - url.without_trailing_slash(), - Cow::Owned(UrlString("example.com/path".into())) + &*url.without_trailing_slash(), + &UrlString("example.com/path".into()) ); + assert!(matches!(url.without_trailing_slash(), Cow::Owned(_))); } } From 06df95adbfe44af2c8bcab1481a8ec4b62cce3b5 Mon Sep 17 00:00:00 2001 From: konsti Date: Tue, 1 Jul 2025 20:39:46 +0200 Subject: [PATCH 153/185] Workaround for panic due to missing global validation in clap (#14368) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clap does not perform global validation, so flag that are declared as overriding can be set at the same time: https://github.com/clap-rs/clap/issues/6049. This would previously cause a panic. We work around this by choosing the yes-value always and writing a warning. An alternative would be erroring when both are set, but it's unclear to me if this may break things we want to support. (`UV_OFFLINE=1 cargo run -q pip --no-offline install tqdm --no-cache` is already banned). Fixes https://github.com/astral-sh/uv/pull/14299 **Test Plan** ``` $ cargo run -q pip --offline install --no-offline tqdm --no-cache warning: Boolean flags on different levels are not correctly supported (https://github.com/clap-rs/clap/issues/6049) × No solution found when resolving dependencies: ╰─▶ Because tqdm was not found in the cache and you require tqdm, we can conclude that your requirements are unsatisfiable. hint: Packages were unavailable because the network was disabled. When the network is disabled, registry packages may only be read from the cache. ``` --- crates/uv-cli/src/options.rs | 62 ++++++---- crates/uv/src/settings.rs | 183 ++++++++++++++++++------------ crates/uv/tests/it/pip_install.rs | 22 ++++ 3 files changed, 170 insertions(+), 97 deletions(-) diff --git a/crates/uv-cli/src/options.rs b/crates/uv-cli/src/options.rs index 656edd43c..f522022a1 100644 --- a/crates/uv-cli/src/options.rs +++ b/crates/uv-cli/src/options.rs @@ -1,7 +1,10 @@ +use anstream::eprintln; + use uv_cache::Refresh; use uv_configuration::ConfigSettings; use uv_resolver::PrereleaseMode; use uv_settings::{Combine, PipOptions, ResolverInstallerOptions, ResolverOptions}; +use uv_warnings::owo_colors::OwoColorize; use crate::{ BuildOptionsArgs, FetchArgs, IndexArgs, InstallerArgs, Maybe, RefreshArgs, ResolverArgs, @@ -9,12 +12,27 @@ use crate::{ }; /// Given a boolean flag pair (like `--upgrade` and `--no-upgrade`), resolve the value of the flag. -pub fn flag(yes: bool, no: bool) -> Option { +pub fn flag(yes: bool, no: bool, name: &str) -> Option { match (yes, no) { (true, false) => Some(true), (false, true) => Some(false), (false, false) => None, - (..) => unreachable!("Clap should make this impossible"), + (..) => { + eprintln!( + "{}{} `{}` and `{}` cannot be used together. \ + Boolean flags on different levels are currently not supported \ + (https://github.com/clap-rs/clap/issues/6049)", + "error".bold().red(), + ":".bold(), + format!("--{name}").green(), + format!("--no-{name}").green(), + ); + // No error forwarding since should eventually be solved on the clap side. + #[allow(clippy::exit)] + { + std::process::exit(2); + } + } } } @@ -26,7 +44,7 @@ impl From for Refresh { refresh_package, } = value; - Self::from_args(flag(refresh, no_refresh), refresh_package) + Self::from_args(flag(refresh, no_refresh, "no-refresh"), refresh_package) } } @@ -53,7 +71,7 @@ impl From for PipOptions { } = args; Self { - upgrade: flag(upgrade, no_upgrade), + upgrade: flag(upgrade, no_upgrade, "no-upgrade"), upgrade_package: Some(upgrade_package), index_strategy, keyring_provider, @@ -66,7 +84,7 @@ impl From for PipOptions { }, config_settings: config_setting .map(|config_settings| config_settings.into_iter().collect::()), - no_build_isolation: flag(no_build_isolation, build_isolation), + no_build_isolation: flag(no_build_isolation, build_isolation, "build-isolation"), no_build_isolation_package: Some(no_build_isolation_package), exclude_newer, link_mode, @@ -96,16 +114,16 @@ impl From for PipOptions { } = args; Self { - reinstall: flag(reinstall, no_reinstall), + reinstall: flag(reinstall, no_reinstall, "reinstall"), reinstall_package: Some(reinstall_package), index_strategy, keyring_provider, config_settings: config_setting .map(|config_settings| config_settings.into_iter().collect::()), - no_build_isolation: flag(no_build_isolation, build_isolation), + no_build_isolation: flag(no_build_isolation, build_isolation, "build-isolation"), exclude_newer, link_mode, - compile_bytecode: flag(compile_bytecode, no_compile_bytecode), + compile_bytecode: flag(compile_bytecode, no_compile_bytecode, "compile-bytecode"), no_sources: if no_sources { Some(true) } else { None }, ..PipOptions::from(index_args) } @@ -140,9 +158,9 @@ impl From for PipOptions { } = args; Self { - upgrade: flag(upgrade, no_upgrade), + upgrade: flag(upgrade, no_upgrade, "upgrade"), upgrade_package: Some(upgrade_package), - reinstall: flag(reinstall, no_reinstall), + reinstall: flag(reinstall, no_reinstall, "reinstall"), reinstall_package: Some(reinstall_package), index_strategy, keyring_provider, @@ -155,11 +173,11 @@ impl From for PipOptions { fork_strategy, config_settings: config_setting .map(|config_settings| config_settings.into_iter().collect::()), - no_build_isolation: flag(no_build_isolation, build_isolation), + no_build_isolation: flag(no_build_isolation, build_isolation, "build-isolation"), no_build_isolation_package: Some(no_build_isolation_package), exclude_newer, link_mode, - compile_bytecode: flag(compile_bytecode, no_compile_bytecode), + compile_bytecode: flag(compile_bytecode, no_compile_bytecode, "compile-bytecode"), no_sources: if no_sources { Some(true) } else { None }, ..PipOptions::from(index_args) } @@ -289,7 +307,7 @@ pub fn resolver_options( .filter_map(Maybe::into_option) .collect() }), - upgrade: flag(upgrade, no_upgrade), + upgrade: flag(upgrade, no_upgrade, "no-upgrade"), upgrade_package: Some(upgrade_package), index_strategy, keyring_provider, @@ -303,13 +321,13 @@ pub fn resolver_options( dependency_metadata: None, config_settings: config_setting .map(|config_settings| config_settings.into_iter().collect::()), - no_build_isolation: flag(no_build_isolation, build_isolation), + no_build_isolation: flag(no_build_isolation, build_isolation, "build-isolation"), no_build_isolation_package: Some(no_build_isolation_package), exclude_newer, link_mode, - no_build: flag(no_build, build), + no_build: flag(no_build, build, "build"), no_build_package: Some(no_build_package), - no_binary: flag(no_binary, binary), + no_binary: flag(no_binary, binary, "binary"), no_binary_package: Some(no_binary_package), no_sources: if no_sources { Some(true) } else { None }, } @@ -386,13 +404,13 @@ pub fn resolver_installer_options( .filter_map(Maybe::into_option) .collect() }), - upgrade: flag(upgrade, no_upgrade), + upgrade: flag(upgrade, no_upgrade, "upgrade"), upgrade_package: if upgrade_package.is_empty() { None } else { Some(upgrade_package) }, - reinstall: flag(reinstall, no_reinstall), + reinstall: flag(reinstall, no_reinstall, "reinstall"), reinstall_package: if reinstall_package.is_empty() { None } else { @@ -410,7 +428,7 @@ pub fn resolver_installer_options( dependency_metadata: None, config_settings: config_setting .map(|config_settings| config_settings.into_iter().collect::()), - no_build_isolation: flag(no_build_isolation, build_isolation), + no_build_isolation: flag(no_build_isolation, build_isolation, "build-isolation"), no_build_isolation_package: if no_build_isolation_package.is_empty() { None } else { @@ -418,14 +436,14 @@ pub fn resolver_installer_options( }, exclude_newer, link_mode, - compile_bytecode: flag(compile_bytecode, no_compile_bytecode), - no_build: flag(no_build, build), + compile_bytecode: flag(compile_bytecode, no_compile_bytecode, "compile-bytecode"), + no_build: flag(no_build, build, "build"), no_build_package: if no_build_package.is_empty() { None } else { Some(no_build_package) }, - no_binary: flag(no_binary, binary), + no_binary: flag(no_binary, binary, "binary"), no_binary_package: if no_binary_package.is_empty() { None } else { diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 58a012d89..004ce5053 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -118,16 +118,20 @@ impl GlobalSettings { }, show_settings: args.show_settings, preview: PreviewMode::from( - flag(args.preview, args.no_preview) + flag(args.preview, args.no_preview, "preview") .combine(workspace.and_then(|workspace| workspace.globals.preview)) .unwrap_or(false), ), python_preference, - python_downloads: flag(args.allow_python_downloads, args.no_python_downloads) - .map(PythonDownloads::from) - .combine(env(env::UV_PYTHON_DOWNLOADS)) - .combine(workspace.and_then(|workspace| workspace.globals.python_downloads)) - .unwrap_or_default(), + python_downloads: flag( + args.allow_python_downloads, + args.no_python_downloads, + "python-downloads", + ) + .map(PythonDownloads::from) + .combine(env(env::UV_PYTHON_DOWNLOADS)) + .combine(workspace.and_then(|workspace| workspace.globals.python_downloads)) + .unwrap_or_default(), // Disable the progress bar with `RUST_LOG` to avoid progress fragments interleaving // with log messages. no_progress: args.no_progress || std::env::var_os(EnvVars::RUST_LOG).is_some(), @@ -161,7 +165,7 @@ pub(crate) struct NetworkSettings { impl NetworkSettings { pub(crate) fn resolve(args: &GlobalArgs, workspace: Option<&FilesystemOptions>) -> Self { - let connectivity = if flag(args.offline, args.no_offline) + let connectivity = if flag(args.offline, args.no_offline, "offline") .combine(workspace.and_then(|workspace| workspace.globals.offline)) .unwrap_or(false) { @@ -169,7 +173,7 @@ impl NetworkSettings { } else { Connectivity::Online }; - let native_tls = flag(args.native_tls, args.no_native_tls) + let native_tls = flag(args.native_tls, args.no_native_tls, "native-tls") .combine(workspace.and_then(|workspace| workspace.globals.native_tls)) .unwrap_or(false); let allow_insecure_host = args @@ -274,8 +278,12 @@ impl InitSettings { (_, _, _) => unreachable!("`app`, `lib`, and `script` are mutually exclusive"), }; - let package = flag(package || build_backend.is_some(), no_package || r#virtual) - .unwrap_or(kind.packaged_by_default()); + let package = flag( + package || build_backend.is_some(), + no_package || r#virtual, + "virtual", + ) + .unwrap_or(kind.packaged_by_default()); let install_mirrors = filesystem .map(|fs| fs.install_mirrors.clone()) @@ -295,7 +303,7 @@ impl InitSettings { build_backend, no_readme: no_readme || bare, author_from, - pin_python: flag(pin_python, no_pin_python).unwrap_or(!bare), + pin_python: flag(pin_python, no_pin_python, "pin-python").unwrap_or(!bare), no_workspace, python: python.and_then(Maybe::into_option), install_mirrors, @@ -398,7 +406,7 @@ impl RunSettings { false, // TODO(blueraft): support only_extra vec![], - flag(all_extras, no_all_extras).unwrap_or_default(), + flag(all_extras, no_all_extras, "all-extras").unwrap_or_default(), ), groups: DependencyGroups::from_args( dev, @@ -411,7 +419,7 @@ impl RunSettings { all_groups, ), editable: EditableMode::from_args(no_editable), - modifications: if flag(exact, inexact).unwrap_or(false) { + modifications: if flag(exact, inexact, "inexact").unwrap_or(false) { Modifications::Exact } else { Modifications::Sufficient @@ -434,7 +442,7 @@ impl RunSettings { package, no_project, no_sync, - active: flag(active, no_active), + active: flag(active, no_active, "active"), python: python.and_then(Maybe::into_option), refresh: Refresh::from(refresh), settings: ResolverInstallerSettings::combine( @@ -1081,7 +1089,7 @@ impl PythonFindSettings { request, show_version, no_project, - system: flag(system, no_system).unwrap_or_default(), + system: flag(system, no_system, "system").unwrap_or_default(), } } } @@ -1116,7 +1124,7 @@ impl PythonPinSettings { Self { request, - resolved: flag(resolved, no_resolved).unwrap_or(false), + resolved: flag(resolved, no_resolved, "resolved").unwrap_or(false), no_project, global, rm, @@ -1195,7 +1203,7 @@ impl SyncSettings { filesystem, ); - let check = flag(check, no_check).unwrap_or_default(); + let check = flag(check, no_check, "check").unwrap_or_default(); let dry_run = if check { DryRun::Check } else { @@ -1207,7 +1215,7 @@ impl SyncSettings { frozen, dry_run, script, - active: flag(active, no_active), + active: flag(active, no_active, "active"), extras: ExtrasSpecification::from_args( extra.unwrap_or_default(), no_extra, @@ -1215,7 +1223,7 @@ impl SyncSettings { false, // TODO(blueraft): support only_extra vec![], - flag(all_extras, no_all_extras).unwrap_or_default(), + flag(all_extras, no_all_extras, "all-extras").unwrap_or_default(), ), groups: DependencyGroups::from_args( dev, @@ -1233,7 +1241,7 @@ impl SyncSettings { no_install_workspace, no_install_package, ), - modifications: if flag(exact, inexact).unwrap_or(true) { + modifications: if flag(exact, inexact, "inexact").unwrap_or(true) { Modifications::Exact } else { Modifications::Sufficient @@ -1437,7 +1445,7 @@ impl AddSettings { Self { locked, frozen, - active: flag(active, no_active), + active: flag(active, no_active, "active"), no_sync, packages, requirements, @@ -1455,7 +1463,7 @@ impl AddSettings { package, script, python: python.and_then(Maybe::into_option), - editable: flag(editable, no_editable), + editable: flag(editable, no_editable, "editable"), extras: extra.unwrap_or_default(), refresh: Refresh::from(refresh), indexes, @@ -1531,7 +1539,7 @@ impl RemoveSettings { Self { locked, frozen, - active: flag(active, no_active), + active: flag(active, no_active, "active"), no_sync, packages, dependency_type, @@ -1603,7 +1611,7 @@ impl VersionSettings { dry_run, locked, frozen, - active: flag(active, no_active), + active: flag(active, no_active, "active"), no_sync, package, python: python.and_then(Maybe::into_option), @@ -1779,7 +1787,7 @@ impl ExportSettings { false, // TODO(blueraft): support only_extra vec![], - flag(all_extras, no_all_extras).unwrap_or_default(), + flag(all_extras, no_all_extras, "all-extras").unwrap_or_default(), ), groups: DependencyGroups::from_args( dev, @@ -1792,7 +1800,7 @@ impl ExportSettings { all_groups, ), editable: EditableMode::from_args(no_editable), - hashes: flag(hashes, no_hashes).unwrap_or(true), + hashes: flag(hashes, no_hashes, "hashes").unwrap_or(true), install_options: InstallOptions::new( no_emit_project, no_emit_workspace, @@ -1801,8 +1809,8 @@ impl ExportSettings { output_file, locked, frozen, - include_annotations: flag(annotate, no_annotate).unwrap_or(true), - include_header: flag(header, no_header).unwrap_or(true), + include_annotations: flag(annotate, no_annotate, "annotate").unwrap_or(true), + include_header: flag(header, no_header, "header").unwrap_or(true), script, python: python.and_then(Maybe::into_option), refresh: Refresh::from(refresh), @@ -1955,30 +1963,42 @@ impl PipCompileSettings { settings: PipSettings::combine( PipOptions { python: python.and_then(Maybe::into_option), - system: flag(system, no_system), - no_build: flag(no_build, build), + system: flag(system, no_system, "system"), + no_build: flag(no_build, build, "build"), no_binary, only_binary, extra, - all_extras: flag(all_extras, no_all_extras), - no_deps: flag(no_deps, deps), + all_extras: flag(all_extras, no_all_extras, "all-extras"), + no_deps: flag(no_deps, deps, "deps"), group: Some(group), output_file, - no_strip_extras: flag(no_strip_extras, strip_extras), - no_strip_markers: flag(no_strip_markers, strip_markers), - no_annotate: flag(no_annotate, annotate), - no_header: flag(no_header, header), + no_strip_extras: flag(no_strip_extras, strip_extras, "strip-extras"), + no_strip_markers: flag(no_strip_markers, strip_markers, "strip-markers"), + no_annotate: flag(no_annotate, annotate, "annotate"), + no_header: flag(no_header, header, "header"), custom_compile_command, - generate_hashes: flag(generate_hashes, no_generate_hashes), + generate_hashes: flag(generate_hashes, no_generate_hashes, "generate-hashes"), python_version, python_platform, - universal: flag(universal, no_universal), + universal: flag(universal, no_universal, "universal"), no_emit_package, - emit_index_url: flag(emit_index_url, no_emit_index_url), - emit_find_links: flag(emit_find_links, no_emit_find_links), - emit_build_options: flag(emit_build_options, no_emit_build_options), - emit_marker_expression: flag(emit_marker_expression, no_emit_marker_expression), - emit_index_annotation: flag(emit_index_annotation, no_emit_index_annotation), + emit_index_url: flag(emit_index_url, no_emit_index_url, "emit-index-url"), + emit_find_links: flag(emit_find_links, no_emit_find_links, "emit-find-links"), + emit_build_options: flag( + emit_build_options, + no_emit_build_options, + "emit-build-options", + ), + emit_marker_expression: flag( + emit_marker_expression, + no_emit_marker_expression, + "emit-marker-expression", + ), + emit_index_annotation: flag( + emit_index_annotation, + no_emit_index_annotation, + "emit-index-annotation", + ), annotation_style, torch_backend, ..PipOptions::from(resolver) @@ -2050,22 +2070,27 @@ impl PipSyncSettings { settings: PipSettings::combine( PipOptions { python: python.and_then(Maybe::into_option), - system: flag(system, no_system), - break_system_packages: flag(break_system_packages, no_break_system_packages), + system: flag(system, no_system, "system"), + break_system_packages: flag( + break_system_packages, + no_break_system_packages, + "break-system-packages", + ), target, prefix, - require_hashes: flag(require_hashes, no_require_hashes), - verify_hashes: flag(verify_hashes, no_verify_hashes), - no_build: flag(no_build, build), + require_hashes: flag(require_hashes, no_require_hashes, "require-hashes"), + verify_hashes: flag(verify_hashes, no_verify_hashes, "verify-hashes"), + no_build: flag(no_build, build, "build"), no_binary, only_binary, allow_empty_requirements: flag( allow_empty_requirements, no_allow_empty_requirements, + "allow-empty-requirements", ), python_version, python_platform, - strict: flag(strict, no_strict), + strict: flag(strict, no_strict, "strict"), torch_backend, ..PipOptions::from(installer) }, @@ -2199,7 +2224,7 @@ impl PipInstallSettings { constraints_from_workspace, overrides_from_workspace, build_constraints_from_workspace, - modifications: if flag(exact, inexact).unwrap_or(false) { + modifications: if flag(exact, inexact, "inexact").unwrap_or(false) { Modifications::Exact } else { Modifications::Sufficient @@ -2208,22 +2233,26 @@ impl PipInstallSettings { settings: PipSettings::combine( PipOptions { python: python.and_then(Maybe::into_option), - system: flag(system, no_system), - break_system_packages: flag(break_system_packages, no_break_system_packages), + system: flag(system, no_system, "system"), + break_system_packages: flag( + break_system_packages, + no_break_system_packages, + "break-system-packages", + ), target, prefix, - no_build: flag(no_build, build), + no_build: flag(no_build, build, "build"), no_binary, only_binary, - strict: flag(strict, no_strict), + strict: flag(strict, no_strict, "strict"), extra, - all_extras: flag(all_extras, no_all_extras), + all_extras: flag(all_extras, no_all_extras, "all-extras"), group: Some(group), - no_deps: flag(no_deps, deps), + no_deps: flag(no_deps, deps, "deps"), python_version, python_platform, - require_hashes: flag(require_hashes, no_require_hashes), - verify_hashes: flag(verify_hashes, no_verify_hashes), + require_hashes: flag(require_hashes, no_require_hashes, "require-hashes"), + verify_hashes: flag(verify_hashes, no_verify_hashes, "verify-hashes"), torch_backend, ..PipOptions::from(installer) }, @@ -2267,8 +2296,12 @@ impl PipUninstallSettings { settings: PipSettings::combine( PipOptions { python: python.and_then(Maybe::into_option), - system: flag(system, no_system), - break_system_packages: flag(break_system_packages, no_break_system_packages), + system: flag(system, no_system, "system"), + break_system_packages: flag( + break_system_packages, + no_break_system_packages, + "break-system-packages", + ), target, prefix, keyring_provider, @@ -2308,8 +2341,8 @@ impl PipFreezeSettings { settings: PipSettings::combine( PipOptions { python: python.and_then(Maybe::into_option), - system: flag(system, no_system), - strict: flag(strict, no_strict), + system: flag(system, no_system, "system"), + strict: flag(strict, no_strict, "strict"), ..PipOptions::default() }, filesystem, @@ -2348,15 +2381,15 @@ impl PipListSettings { } = args; Self { - editable: flag(editable, exclude_editable), + editable: flag(editable, exclude_editable, "exclude-editable"), exclude, format, - outdated: flag(outdated, no_outdated).unwrap_or(false), + outdated: flag(outdated, no_outdated, "outdated").unwrap_or(false), settings: PipSettings::combine( PipOptions { python: python.and_then(Maybe::into_option), - system: flag(system, no_system), - strict: flag(strict, no_strict), + system: flag(system, no_system, "system"), + strict: flag(strict, no_strict, "strict"), ..PipOptions::from(fetch) }, filesystem, @@ -2393,8 +2426,8 @@ impl PipShowSettings { settings: PipSettings::combine( PipOptions { python: python.and_then(Maybe::into_option), - system: flag(system, no_system), - strict: flag(strict, no_strict), + system: flag(system, no_system, "system"), + strict: flag(strict, no_strict, "strict"), ..PipOptions::default() }, filesystem, @@ -2442,8 +2475,8 @@ impl PipTreeSettings { settings: PipSettings::combine( PipOptions { python: python.and_then(Maybe::into_option), - system: flag(system, no_system), - strict: flag(strict, no_strict), + system: flag(system, no_system, "system"), + strict: flag(strict, no_strict, "strict"), ..PipOptions::from(fetch) }, filesystem, @@ -2471,7 +2504,7 @@ impl PipCheckSettings { settings: PipSettings::combine( PipOptions { python: python.and_then(Maybe::into_option), - system: flag(system, no_system), + system: flag(system, no_system, "system"), ..PipOptions::default() }, filesystem, @@ -2538,15 +2571,15 @@ impl BuildSettings { sdist, wheel, list, - build_logs: flag(build_logs, no_build_logs).unwrap_or(true), + build_logs: flag(build_logs, no_build_logs, "build-logs").unwrap_or(true), build_constraints: build_constraints .into_iter() .filter_map(Maybe::into_option) .collect(), force_pep517, hash_checking: HashCheckingMode::from_args( - flag(require_hashes, no_require_hashes), - flag(verify_hashes, no_verify_hashes), + flag(require_hashes, no_require_hashes, "require-hashes"), + flag(verify_hashes, no_verify_hashes, "verify-hashes"), ), python: python.and_then(Maybe::into_option), refresh: Refresh::from(refresh), @@ -2605,7 +2638,7 @@ impl VenvSettings { settings: PipSettings::combine( PipOptions { python: python.and_then(Maybe::into_option), - system: flag(system, no_system), + system: flag(system, no_system, "system"), index_strategy, keyring_provider, exclude_newer, diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index 91da3ce81..76b108a81 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -11486,3 +11486,25 @@ fn pep_751_dependency() -> Result<()> { Ok(()) } + +/// Test that we show an error instead of panicking for conflicting arguments in different levels, +/// which are not caught by clap. +#[test] +fn conflicting_flags_clap_bug() { + let context = TestContext::new("3.12"); + + uv_snapshot!(context.filters(), context.command() + .arg("pip") + .arg("--offline") + .arg("install") + .arg("--no-offline") + .arg("tqdm"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: `--offline` and `--no-offline` cannot be used together. Boolean flags on different levels are currently not supported (https://github.com/clap-rs/clap/issues/6049) + " + ); +} From 87e9ccfb92e2761433bebddbdd68895de0e20abd Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Tue, 1 Jul 2025 15:30:44 -0500 Subject: [PATCH 154/185] Bump version to 0.7.18 (#14402) --- CHANGELOG.md | 34 +++++++++++++++++++++++++++ Cargo.lock | 6 ++--- crates/uv-build/Cargo.toml | 2 +- crates/uv-build/pyproject.toml | 2 +- crates/uv-version/Cargo.toml | 2 +- crates/uv/Cargo.toml | 2 +- docs/concepts/build-backend.md | 2 +- docs/getting-started/installation.md | 4 ++-- docs/guides/integration/aws-lambda.md | 4 ++-- docs/guides/integration/docker.md | 10 ++++---- docs/guides/integration/github.md | 2 +- docs/guides/integration/pre-commit.md | 10 ++++---- pyproject.toml | 2 +- 13 files changed, 58 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc982ad71..2605248ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,40 @@ + +## 0.7.18 + +### Python + +- Added arm64 Windows Python 3.11, 3.12, 3.13, and 3.14 + + These are not downloaded by default, since x86-64 Python has broader ecosystem support on Windows. + However, they can be requested with `cpython--windows-aarch64`. + +### Enhancements + +- Keep track of retries in `ManagedPythonDownload::fetch_with_retry` ([#14378](https://github.com/astral-sh/uv/pull/14378)) +- Reuse build (virtual) environments across resolution and installation ([#14338](https://github.com/astral-sh/uv/pull/14338)) +- Improve trace message for cached Python interpreter query ([#14328](https://github.com/astral-sh/uv/pull/14328)) +- Use parsed URLs for conflicting URL error message ([#14380](https://github.com/astral-sh/uv/pull/14380)) + +### Preview features + +- Ignore invalid build backend settings when not building ([#14372](https://github.com/astral-sh/uv/pull/14372)) + +### Bug fixes + +- Fix equals-star and tilde-equals with `python_version` and `python_full_version` ([#14271](https://github.com/astral-sh/uv/pull/14271)) +- Include the canonical path in the interpreter query cache key ([#14331](https://github.com/astral-sh/uv/pull/14331)) +- Only drop build directories on program exit ([#14304](https://github.com/astral-sh/uv/pull/14304)) +- Error instead of panic on conflict between global and subcommand flags ([#14368](https://github.com/astral-sh/uv/pull/14368)) +- Consistently normalize trailing slashes on URLs with no path segments ([#14349](https://github.com/astral-sh/uv/pull/14349)) + +### Documentation + +- Add instructions for publishing to JFrog's Artifactory ([#14253](https://github.com/astral-sh/uv/pull/14253)) +- Edits to the build backend documentation ([#14376](https://github.com/astral-sh/uv/pull/14376)) + ## 0.7.17 ### Bug fixes diff --git a/Cargo.lock b/Cargo.lock index 802b069f5..881cd8423 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4603,7 +4603,7 @@ dependencies = [ [[package]] name = "uv" -version = "0.7.17" +version = "0.7.18" dependencies = [ "anstream", "anyhow", @@ -4767,7 +4767,7 @@ dependencies = [ [[package]] name = "uv-build" -version = "0.7.17" +version = "0.7.18" dependencies = [ "anyhow", "uv-build-backend", @@ -5957,7 +5957,7 @@ dependencies = [ [[package]] name = "uv-version" -version = "0.7.17" +version = "0.7.18" [[package]] name = "uv-virtualenv" diff --git a/crates/uv-build/Cargo.toml b/crates/uv-build/Cargo.toml index 8897a421d..83c37cd15 100644 --- a/crates/uv-build/Cargo.toml +++ b/crates/uv-build/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-build" -version = "0.7.17" +version = "0.7.18" edition.workspace = true rust-version.workspace = true homepage.workspace = true diff --git a/crates/uv-build/pyproject.toml b/crates/uv-build/pyproject.toml index ae0ecde47..f48e0ddff 100644 --- a/crates/uv-build/pyproject.toml +++ b/crates/uv-build/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uv-build" -version = "0.7.17" +version = "0.7.18" description = "The uv build backend" authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }] requires-python = ">=3.8" diff --git a/crates/uv-version/Cargo.toml b/crates/uv-version/Cargo.toml index 2d0c3d096..c17f17695 100644 --- a/crates/uv-version/Cargo.toml +++ b/crates/uv-version/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-version" -version = "0.7.17" +version = "0.7.18" edition = { workspace = true } rust-version = { workspace = true } homepage = { workspace = true } diff --git a/crates/uv/Cargo.toml b/crates/uv/Cargo.toml index 5f0ea848e..1b8d878ee 100644 --- a/crates/uv/Cargo.toml +++ b/crates/uv/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv" -version = "0.7.17" +version = "0.7.18" edition = { workspace = true } rust-version = { workspace = true } homepage = { workspace = true } diff --git a/docs/concepts/build-backend.md b/docs/concepts/build-backend.md index dd7685756..c78fa55a9 100644 --- a/docs/concepts/build-backend.md +++ b/docs/concepts/build-backend.md @@ -24,7 +24,7 @@ To use uv as a build backend in an existing project, add `uv_build` to the ```toml title="pyproject.toml" [build-system] -requires = ["uv_build>=0.7.17,<0.8.0"] +requires = ["uv_build>=0.7.18,<0.8.0"] build-backend = "uv_build" ``` diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 62f9f50ae..227b4df43 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -25,7 +25,7 @@ uv provides a standalone installer to download and install uv: Request a specific version by including it in the URL: ```console - $ curl -LsSf https://astral.sh/uv/0.7.17/install.sh | sh + $ curl -LsSf https://astral.sh/uv/0.7.18/install.sh | sh ``` === "Windows" @@ -41,7 +41,7 @@ uv provides a standalone installer to download and install uv: Request a specific version by including it in the URL: ```pwsh-session - PS> powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/0.7.17/install.ps1 | iex" + PS> powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/0.7.18/install.ps1 | iex" ``` !!! tip diff --git a/docs/guides/integration/aws-lambda.md b/docs/guides/integration/aws-lambda.md index a0ee99671..db45f1016 100644 --- a/docs/guides/integration/aws-lambda.md +++ b/docs/guides/integration/aws-lambda.md @@ -92,7 +92,7 @@ the second stage, we'll copy this directory over to the final image, omitting th other unnecessary files. ```dockerfile title="Dockerfile" -FROM ghcr.io/astral-sh/uv:0.7.17 AS uv +FROM ghcr.io/astral-sh/uv:0.7.18 AS uv # First, bundle the dependencies into the task root. FROM public.ecr.aws/lambda/python:3.13 AS builder @@ -334,7 +334,7 @@ And confirm that opening http://127.0.0.1:8000/ in a web browser displays, "Hell Finally, we'll update the Dockerfile to include the local library in the deployment package: ```dockerfile title="Dockerfile" -FROM ghcr.io/astral-sh/uv:0.7.17 AS uv +FROM ghcr.io/astral-sh/uv:0.7.18 AS uv # First, bundle the dependencies into the task root. FROM public.ecr.aws/lambda/python:3.13 AS builder diff --git a/docs/guides/integration/docker.md b/docs/guides/integration/docker.md index 48d7b6399..27416a0fa 100644 --- a/docs/guides/integration/docker.md +++ b/docs/guides/integration/docker.md @@ -31,7 +31,7 @@ $ docker run --rm -it ghcr.io/astral-sh/uv:debian uv --help The following distroless images are available: - `ghcr.io/astral-sh/uv:latest` -- `ghcr.io/astral-sh/uv:{major}.{minor}.{patch}`, e.g., `ghcr.io/astral-sh/uv:0.7.17` +- `ghcr.io/astral-sh/uv:{major}.{minor}.{patch}`, e.g., `ghcr.io/astral-sh/uv:0.7.18` - `ghcr.io/astral-sh/uv:{major}.{minor}`, e.g., `ghcr.io/astral-sh/uv:0.7` (the latest patch version) @@ -75,7 +75,7 @@ And the following derived images are available: As with the distroless image, each derived image is published with uv version tags as `ghcr.io/astral-sh/uv:{major}.{minor}.{patch}-{base}` and -`ghcr.io/astral-sh/uv:{major}.{minor}-{base}`, e.g., `ghcr.io/astral-sh/uv:0.7.17-alpine`. +`ghcr.io/astral-sh/uv:{major}.{minor}-{base}`, e.g., `ghcr.io/astral-sh/uv:0.7.18-alpine`. For more details, see the [GitHub Container](https://github.com/astral-sh/uv/pkgs/container/uv) page. @@ -113,7 +113,7 @@ Note this requires `curl` to be available. In either case, it is best practice to pin to a specific uv version, e.g., with: ```dockerfile -COPY --from=ghcr.io/astral-sh/uv:0.7.17 /uv /uvx /bin/ +COPY --from=ghcr.io/astral-sh/uv:0.7.18 /uv /uvx /bin/ ``` !!! tip @@ -131,7 +131,7 @@ COPY --from=ghcr.io/astral-sh/uv:0.7.17 /uv /uvx /bin/ Or, with the installer: ```dockerfile -ADD https://astral.sh/uv/0.7.17/install.sh /uv-installer.sh +ADD https://astral.sh/uv/0.7.18/install.sh /uv-installer.sh ``` ### Installing a project @@ -557,5 +557,5 @@ Verified OK !!! tip These examples use `latest`, but best practice is to verify the attestation for a specific - version tag, e.g., `ghcr.io/astral-sh/uv:0.7.17`, or (even better) the specific image digest, + version tag, e.g., `ghcr.io/astral-sh/uv:0.7.18`, or (even better) the specific image digest, such as `ghcr.io/astral-sh/uv:0.5.27@sha256:5adf09a5a526f380237408032a9308000d14d5947eafa687ad6c6a2476787b4f`. diff --git a/docs/guides/integration/github.md b/docs/guides/integration/github.md index f58fe2782..60a18b8b4 100644 --- a/docs/guides/integration/github.md +++ b/docs/guides/integration/github.md @@ -47,7 +47,7 @@ jobs: uses: astral-sh/setup-uv@v5 with: # Install a specific version of uv. - version: "0.7.17" + version: "0.7.18" ``` ## Setting up Python diff --git a/docs/guides/integration/pre-commit.md b/docs/guides/integration/pre-commit.md index 71fe7394f..dbbc88462 100644 --- a/docs/guides/integration/pre-commit.md +++ b/docs/guides/integration/pre-commit.md @@ -19,7 +19,7 @@ To make sure your `uv.lock` file is up to date even if your `pyproject.toml` fil repos: - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.7.17 + rev: 0.7.18 hooks: - id: uv-lock ``` @@ -30,7 +30,7 @@ To keep a `requirements.txt` file in sync with your `uv.lock` file: repos: - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.7.17 + rev: 0.7.18 hooks: - id: uv-export ``` @@ -41,7 +41,7 @@ To compile requirements files: repos: - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.7.17 + rev: 0.7.18 hooks: # Compile requirements - id: pip-compile @@ -54,7 +54,7 @@ To compile alternative requirements files, modify `args` and `files`: repos: - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.7.17 + rev: 0.7.18 hooks: # Compile requirements - id: pip-compile @@ -68,7 +68,7 @@ To run the hook over multiple files at the same time, add additional entries: repos: - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.7.17 + rev: 0.7.18 hooks: # Compile requirements - id: pip-compile diff --git a/pyproject.toml b/pyproject.toml index 078bd4ebd..7e73fa84c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "uv" -version = "0.7.17" +version = "0.7.18" description = "An extremely fast Python package and project manager, written in Rust." authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }] requires-python = ">=3.8" From e40d3d5dffe03ce727ae554e9479d4c5b7c0cd53 Mon Sep 17 00:00:00 2001 From: John Mumm Date: Wed, 2 Jul 2025 13:50:35 +0200 Subject: [PATCH 155/185] Re-enable Artifactory in the registries integration test (#14408) Having worked out the account issue, I've re-enabled Artifactory in the registries test. --- .github/workflows/ci.yml | 6 +++--- scripts/registries-test.py | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 701699239..35ceb6875 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1570,9 +1570,9 @@ jobs: run: ./uv run -p ${{ env.PYTHON_VERSION }} scripts/registries-test.py --uv ./uv --color always --all env: RUST_LOG: uv=debug - # UV_TEST_ARTIFACTORY_TOKEN: ${{ secrets.UV_TEST_ARTIFACTORY_TOKEN }} - # UV_TEST_ARTIFACTORY_URL: ${{ secrets.UV_TEST_ARTIFACTORY_URL }} - # UV_TEST_ARTIFACTORY_USERNAME: ${{ secrets.UV_TEST_ARTIFACTORY_USERNAME }} + UV_TEST_ARTIFACTORY_TOKEN: ${{ secrets.UV_TEST_ARTIFACTORY_TOKEN }} + UV_TEST_ARTIFACTORY_URL: ${{ secrets.UV_TEST_ARTIFACTORY_URL }} + UV_TEST_ARTIFACTORY_USERNAME: ${{ secrets.UV_TEST_ARTIFACTORY_USERNAME }} UV_TEST_AWS_URL: ${{ secrets.UV_TEST_AWS_URL }} UV_TEST_AWS_USERNAME: aws UV_TEST_AZURE_TOKEN: ${{ secrets.UV_TEST_AZURE_TOKEN }} diff --git a/scripts/registries-test.py b/scripts/registries-test.py index b6bbf9b59..2d4c1d2aa 100644 --- a/scripts/registries-test.py +++ b/scripts/registries-test.py @@ -56,8 +56,7 @@ DEFAULT_TIMEOUT = 30 DEFAULT_PKG_NAME = "astral-registries-test-pkg" KNOWN_REGISTRIES = [ - # TODO(john): Restore this when subscription starts up again - # "artifactory", + "artifactory", "azure", "aws", "cloudsmith", From bf5dcf99294588015feacebb0720f6da54af3c9a Mon Sep 17 00:00:00 2001 From: konsti Date: Wed, 2 Jul 2025 15:25:56 +0200 Subject: [PATCH 156/185] Reduce index credential stashing code duplication (#14419) Reduces some duplicate code around index credentials. --- crates/uv-distribution-types/src/index_url.rs | 13 ++++++ crates/uv/src/commands/build_frontend.rs | 12 +---- crates/uv/src/commands/pip/compile.rs | 12 +---- crates/uv/src/commands/pip/install.rs | 12 +---- crates/uv/src/commands/pip/sync.rs | 12 +---- crates/uv/src/commands/project/add.rs | 11 +---- crates/uv/src/commands/project/lock.rs | 11 +---- crates/uv/src/commands/project/mod.rs | 44 ++----------------- crates/uv/src/commands/project/sync.rs | 11 +---- crates/uv/src/commands/venv.rs | 11 +---- 10 files changed, 25 insertions(+), 124 deletions(-) diff --git a/crates/uv-distribution-types/src/index_url.rs b/crates/uv-distribution-types/src/index_url.rs index eb91e852e..0290018f1 100644 --- a/crates/uv-distribution-types/src/index_url.rs +++ b/crates/uv-distribution-types/src/index_url.rs @@ -462,6 +462,19 @@ impl<'a> IndexLocations { indexes } } + + /// Add all authenticated sources to the cache. + pub fn cache_index_credentials(&self) { + for index in self.allowed_indexes() { + if let Some(credentials) = index.credentials() { + let credentials = Arc::new(credentials); + uv_auth::store_credentials(index.raw_url(), credentials.clone()); + if let Some(root_url) = index.root_url() { + uv_auth::store_credentials(&root_url, credentials.clone()); + } + } + } + } } impl From<&IndexLocations> for uv_auth::Indexes { diff --git a/crates/uv/src/commands/build_frontend.rs b/crates/uv/src/commands/build_frontend.rs index 9b97b40b1..bccb99fae 100644 --- a/crates/uv/src/commands/build_frontend.rs +++ b/crates/uv/src/commands/build_frontend.rs @@ -3,7 +3,6 @@ use std::fmt::Write as _; use std::io::Write as _; use std::path::{Path, PathBuf}; use std::str::FromStr; -use std::sync::Arc; use std::{fmt, io}; use anyhow::{Context, Result}; @@ -504,16 +503,7 @@ async fn build_package( .await? .into_interpreter(); - // Add all authenticated sources to the cache. - for index in index_locations.allowed_indexes() { - if let Some(credentials) = index.credentials() { - let credentials = Arc::new(credentials); - uv_auth::store_credentials(index.raw_url(), credentials.clone()); - if let Some(root_url) = index.root_url() { - uv_auth::store_credentials(&root_url, credentials.clone()); - } - } - } + index_locations.cache_index_credentials(); // Read build constraints. let build_constraints = diff --git a/crates/uv/src/commands/pip/compile.rs b/crates/uv/src/commands/pip/compile.rs index db80c2a8a..3aa3fb0e9 100644 --- a/crates/uv/src/commands/pip/compile.rs +++ b/crates/uv/src/commands/pip/compile.rs @@ -3,7 +3,6 @@ use std::env; use std::ffi::OsStr; use std::path::{Path, PathBuf}; use std::str::FromStr; -use std::sync::Arc; use anyhow::{Result, anyhow}; use itertools::Itertools; @@ -388,16 +387,7 @@ pub(crate) async fn pip_compile( no_index, ); - // Add all authenticated sources to the cache. - for index in index_locations.allowed_indexes() { - if let Some(credentials) = index.credentials() { - let credentials = Arc::new(credentials); - uv_auth::store_credentials(index.raw_url(), credentials.clone()); - if let Some(root_url) = index.root_url() { - uv_auth::store_credentials(&root_url, credentials.clone()); - } - } - } + index_locations.cache_index_credentials(); // Determine the PyTorch backend. let torch_backend = torch_backend diff --git a/crates/uv/src/commands/pip/install.rs b/crates/uv/src/commands/pip/install.rs index a92c36665..b7d32dd94 100644 --- a/crates/uv/src/commands/pip/install.rs +++ b/crates/uv/src/commands/pip/install.rs @@ -1,7 +1,6 @@ use std::collections::{BTreeMap, BTreeSet}; use std::fmt::Write; use std::path::PathBuf; -use std::sync::Arc; use anyhow::Context; use itertools::Itertools; @@ -334,16 +333,7 @@ pub(crate) async fn pip_install( no_index, ); - // Add all authenticated sources to the cache. - for index in index_locations.allowed_indexes() { - if let Some(credentials) = index.credentials() { - let credentials = Arc::new(credentials); - uv_auth::store_credentials(index.raw_url(), credentials.clone()); - if let Some(root_url) = index.root_url() { - uv_auth::store_credentials(&root_url, credentials.clone()); - } - } - } + index_locations.cache_index_credentials(); // Determine the PyTorch backend. let torch_backend = torch_backend diff --git a/crates/uv/src/commands/pip/sync.rs b/crates/uv/src/commands/pip/sync.rs index e5145400a..ab4c42ce5 100644 --- a/crates/uv/src/commands/pip/sync.rs +++ b/crates/uv/src/commands/pip/sync.rs @@ -1,6 +1,5 @@ use std::collections::{BTreeMap, BTreeSet}; use std::fmt::Write; -use std::sync::Arc; use anyhow::{Context, Result}; use owo_colors::OwoColorize; @@ -267,16 +266,7 @@ pub(crate) async fn pip_sync( no_index, ); - // Add all authenticated sources to the cache. - for index in index_locations.allowed_indexes() { - if let Some(credentials) = index.credentials() { - let credentials = Arc::new(credentials); - uv_auth::store_credentials(index.raw_url(), credentials.clone()); - if let Some(root_url) = index.root_url() { - uv_auth::store_credentials(&root_url, credentials.clone()); - } - } - } + index_locations.cache_index_credentials(); // Determine the PyTorch backend. let torch_backend = torch_backend diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index bd3b49a3f..719821df5 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -374,16 +374,7 @@ pub(crate) async fn add( let hasher = HashStrategy::default(); let sources = SourceStrategy::Enabled; - // Add all authenticated sources to the cache. - for index in settings.resolver.index_locations.allowed_indexes() { - if let Some(credentials) = index.credentials() { - let credentials = Arc::new(credentials); - uv_auth::store_credentials(index.raw_url(), credentials.clone()); - if let Some(root_url) = index.root_url() { - uv_auth::store_credentials(&root_url, credentials.clone()); - } - } - } + settings.resolver.index_locations.cache_index_credentials(); // Initialize the registry client. let client = RegistryClientBuilder::try_from(client_builder)? diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index 2bcb68eb3..cd4242833 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -593,16 +593,7 @@ async fn do_lock( .keyring(*keyring_provider) .allow_insecure_host(network_settings.allow_insecure_host.clone()); - // Add all authenticated sources to the cache. - for index in index_locations.allowed_indexes() { - if let Some(credentials) = index.credentials() { - let credentials = Arc::new(credentials); - uv_auth::store_credentials(index.raw_url(), credentials.clone()); - if let Some(root_url) = index.root_url() { - uv_auth::store_credentials(&root_url, credentials.clone()); - } - } - } + index_locations.cache_index_credentials(); for index in target.indexes() { if let Some(credentials) = index.credentials() { diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index c84253eaa..5ed6293e2 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -1626,16 +1626,7 @@ pub(crate) async fn resolve_names( .keyring(*keyring_provider) .allow_insecure_host(network_settings.allow_insecure_host.clone()); - // Add all authenticated sources to the cache. - for index in index_locations.allowed_indexes() { - if let Some(credentials) = index.credentials() { - let credentials = Arc::new(credentials); - uv_auth::store_credentials(index.raw_url(), credentials.clone()); - if let Some(root_url) = index.root_url() { - uv_auth::store_credentials(&root_url, credentials.clone()); - } - } - } + index_locations.cache_index_credentials(); // Initialize the registry client. let client = RegistryClientBuilder::try_from(client_builder)? @@ -1797,16 +1788,7 @@ pub(crate) async fn resolve_environment( let marker_env = interpreter.resolver_marker_environment(); let python_requirement = PythonRequirement::from_interpreter(interpreter); - // Add all authenticated sources to the cache. - for index in index_locations.allowed_indexes() { - if let Some(credentials) = index.credentials() { - let credentials = Arc::new(credentials); - uv_auth::store_credentials(index.raw_url(), credentials.clone()); - if let Some(root_url) = index.root_url() { - uv_auth::store_credentials(&root_url, credentials.clone()); - } - } - } + index_locations.cache_index_credentials(); // Initialize the registry client. let client = RegistryClientBuilder::try_from(client_builder)? @@ -1978,16 +1960,7 @@ pub(crate) async fn sync_environment( let interpreter = venv.interpreter(); let tags = venv.interpreter().tags()?; - // Add all authenticated sources to the cache. - for index in index_locations.allowed_indexes() { - if let Some(credentials) = index.credentials() { - let credentials = Arc::new(credentials); - uv_auth::store_credentials(index.raw_url(), credentials.clone()); - if let Some(root_url) = index.root_url() { - uv_auth::store_credentials(&root_url, credentials.clone()); - } - } - } + index_locations.cache_index_credentials(); // Initialize the registry client. let client = RegistryClientBuilder::try_from(client_builder)? @@ -2193,16 +2166,7 @@ pub(crate) async fn update_environment( } } - // Add all authenticated sources to the cache. - for index in index_locations.allowed_indexes() { - if let Some(credentials) = index.credentials() { - let credentials = Arc::new(credentials); - uv_auth::store_credentials(index.raw_url(), credentials.clone()); - if let Some(root_url) = index.root_url() { - uv_auth::store_credentials(&root_url, credentials.clone()); - } - } - } + index_locations.cache_index_credentials(); // Initialize the registry client. let client = RegistryClientBuilder::try_from(client_builder)? diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index 19848ee02..dc9f0dcbb 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -682,16 +682,7 @@ pub(super) async fn do_sync( // If necessary, convert editable to non-editable distributions. let resolution = apply_editable_mode(resolution, editable); - // Add all authenticated sources to the cache. - for index in index_locations.allowed_indexes() { - if let Some(credentials) = index.credentials() { - let credentials = Arc::new(credentials); - uv_auth::store_credentials(index.raw_url(), credentials.clone()); - if let Some(root_url) = index.root_url() { - uv_auth::store_credentials(&root_url, credentials.clone()); - } - } - } + index_locations.cache_index_credentials(); // Populate credentials from the target. store_credentials_from_target(target); diff --git a/crates/uv/src/commands/venv.rs b/crates/uv/src/commands/venv.rs index fe20634d0..9334d844d 100644 --- a/crates/uv/src/commands/venv.rs +++ b/crates/uv/src/commands/venv.rs @@ -242,16 +242,7 @@ async fn venv_impl( python.into_interpreter() }; - // Add all authenticated sources to the cache. - for index in index_locations.allowed_indexes() { - if let Some(credentials) = index.credentials() { - let credentials = Arc::new(credentials); - uv_auth::store_credentials(index.raw_url(), credentials.clone()); - if let Some(root_url) = index.root_url() { - uv_auth::store_credentials(&root_url, credentials.clone()); - } - } - } + index_locations.cache_index_credentials(); // Check if the discovered Python version is incompatible with the current workspace if let Some(requires_python) = requires_python { From b0db548c8070bf4c1fc78c3f65e34f30ab1d3b64 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Wed, 2 Jul 2025 08:39:58 -0500 Subject: [PATCH 157/185] Bump the test timeout from 90s -> 120s (#14170) In hopes of resolving https://github.com/astral-sh/uv/issues/14158 We should also see why the tests are so slow though. --- .config/nextest.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.config/nextest.toml b/.config/nextest.toml index c063bb861..cc1a18dbe 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -1,4 +1,4 @@ [profile.default] # Mark tests that take longer than 10s as slow. -# Terminate after 90s as a stop-gap measure to terminate on deadlock. -slow-timeout = { period = "10s", terminate-after = 9 } +# Terminate after 120s as a stop-gap measure to terminate on deadlock. +slow-timeout = { period = "10s", terminate-after = 12 } From a7aa46acc571f20cf3fb2ce6fb26219ccbbd44f6 Mon Sep 17 00:00:00 2001 From: konsti Date: Wed, 2 Jul 2025 16:02:03 +0200 Subject: [PATCH 158/185] Add a "choosing a build backend" section to the docs (#14295) I think the build backend docs as a whole are now ready for review. I only made a small change here. --------- Co-authored-by: Zanie Blue --- docs/concepts/build-backend.md | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/docs/concepts/build-backend.md b/docs/concepts/build-backend.md index c78fa55a9..d70f00282 100644 --- a/docs/concepts/build-backend.md +++ b/docs/concepts/build-backend.md @@ -12,13 +12,26 @@ uv supports all build backends (as specified by [PEP 517](https://peps.python.or also provides a native build backend (`uv_build`) that integrates tightly with uv to improve performance and user experience. +## Choosing a build backend + +The uv build backend is a good choice for most Python projects that are using uv. It has reasonable +defaults, with the goal of requiring zero configuration for most users, but provides flexible +configuration that allows most Python project structures. It integrates tightly with uv, to improve +messaging and user experience. It validates project metadata and structures, preventing common +mistakes. And, finally, it's very fast. + +The uv build backend currently **only supports pure Python code**. An alternative backend is +required to build a +[library with extension modules](../concepts/projects/init.md#projects-with-extension-modules). + +!!! tip + + While the backend supports a number of options for configuring your project structure, when build scripts or + a more flexible project layout are required, consider using the + [hatchling](https://hatch.pypa.io/latest/config/build/#build-system) build backend instead. + ## Using the uv build backend -!!! important - - The uv build backend currently **only supports pure Python code**. An alternative backend is to - build a [library with extension modules](../concepts/projects/init.md#projects-with-extension-modules). - To use uv as a build backend in an existing project, add `uv_build` to the [`[build-system]`](../concepts/projects/config.md#build-systems) section in your `pyproject.toml`: From 43f67a4a4cbbd6d8b5c0451d2e79dfca935910dc Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Wed, 2 Jul 2025 09:08:45 -0500 Subject: [PATCH 159/185] Update the tilde version specifier warning to include more context (#14335) Follows https://github.com/astral-sh/uv/pull/14008 --- .../src/requires_python.rs | 25 +----- crates/uv-pep440/src/lib.rs | 2 +- crates/uv-pep440/src/version_specifier.rs | 89 +++++++++++++++++-- crates/uv/src/commands/project/mod.rs | 26 +++++- crates/uv/tests/it/lock.rs | 6 +- 5 files changed, 115 insertions(+), 33 deletions(-) diff --git a/crates/uv-distribution-types/src/requires_python.rs b/crates/uv-distribution-types/src/requires_python.rs index cedfff843..49a4fd5c4 100644 --- a/crates/uv-distribution-types/src/requires_python.rs +++ b/crates/uv-distribution-types/src/requires_python.rs @@ -5,11 +5,10 @@ use version_ranges::Ranges; use uv_distribution_filename::WheelFilename; use uv_pep440::{ LowerBound, UpperBound, Version, VersionSpecifier, VersionSpecifiers, - release_specifier_to_range, release_specifiers_to_ranges, + release_specifiers_to_ranges, }; use uv_pep508::{MarkerExpression, MarkerTree, MarkerValueVersion}; use uv_platform_tags::{AbiTag, LanguageTag}; -use uv_warnings::warn_user_once; /// The `Requires-Python` requirement specifier. /// @@ -67,27 +66,7 @@ impl RequiresPython { ) -> Option { // Convert to PubGrub range and perform an intersection. let range = specifiers - .map(|specs| { - // Warn if there’s exactly one `~=` specifier without a patch. - if let [spec] = &specs[..] { - if spec.is_tilde_without_patch() { - if let Some((lo_b, hi_b)) = release_specifier_to_range(spec.clone(), false) - .bounding_range() - .map(|(l, u)| (l.cloned(), u.cloned())) - { - let lo_spec = LowerBound::new(lo_b).specifier().unwrap(); - let hi_spec = UpperBound::new(hi_b).specifier().unwrap(); - warn_user_once!( - "The release specifier (`{spec}`) contains a compatible release \ - match without a patch version. This will be interpreted as \ - `{lo_spec}, {hi_spec}`. Did you mean `{spec}.0` to freeze the \ - minor version?" - ); - } - } - } - release_specifiers_to_ranges(specs.clone()) - }) + .map(|specs| release_specifiers_to_ranges(specs.clone())) .reduce(|acc, r| acc.intersection(&r))?; // If the intersection is empty, return `None`. diff --git a/crates/uv-pep440/src/lib.rs b/crates/uv-pep440/src/lib.rs index 3d2e256ae..0e8b50e72 100644 --- a/crates/uv-pep440/src/lib.rs +++ b/crates/uv-pep440/src/lib.rs @@ -34,7 +34,7 @@ pub use { VersionPatternParseError, }, version_specifier::{ - VersionSpecifier, VersionSpecifierBuildError, VersionSpecifiers, + TildeVersionSpecifier, VersionSpecifier, VersionSpecifierBuildError, VersionSpecifiers, VersionSpecifiersParseError, }, }; diff --git a/crates/uv-pep440/src/version_specifier.rs b/crates/uv-pep440/src/version_specifier.rs index bbbe440bf..e111c5118 100644 --- a/crates/uv-pep440/src/version_specifier.rs +++ b/crates/uv-pep440/src/version_specifier.rs @@ -665,11 +665,6 @@ impl VersionSpecifier { | Operator::NotEqual => false, } } - - /// Returns true if this is a `~=` specifier without a patch version (e.g. `~=3.11`). - pub fn is_tilde_without_patch(&self) -> bool { - self.operator == Operator::TildeEqual && self.version.release().len() == 2 - } } impl FromStr for VersionSpecifier { @@ -893,6 +888,90 @@ pub(crate) fn parse_version_specifiers( Ok(version_ranges) } +/// A simple `~=` version specifier with a major, minor and (optional) patch version, e.g., `~=3.13` +/// or `~=3.13.0`. +#[derive(Clone, Debug)] +pub struct TildeVersionSpecifier<'a> { + inner: Cow<'a, VersionSpecifier>, +} + +impl<'a> TildeVersionSpecifier<'a> { + /// Create a new [`TildeVersionSpecifier`] from a [`VersionSpecifier`] value. + /// + /// If a [`Operator::TildeEqual`] is not used, or the version includes more than minor and patch + /// segments, this will return [`None`]. + pub fn from_specifier(specifier: VersionSpecifier) -> Option> { + TildeVersionSpecifier::new(Cow::Owned(specifier)) + } + + /// Create a new [`TildeVersionSpecifier`] from a [`VersionSpecifier`] reference. + /// + /// See [`TildeVersionSpecifier::from_specifier`]. + pub fn from_specifier_ref( + specifier: &'a VersionSpecifier, + ) -> Option> { + TildeVersionSpecifier::new(Cow::Borrowed(specifier)) + } + + fn new(specifier: Cow<'a, VersionSpecifier>) -> Option { + if specifier.operator != Operator::TildeEqual { + return None; + } + if specifier.version().release().len() < 2 || specifier.version().release().len() > 3 { + return None; + } + if specifier.version().any_prerelease() + || specifier.version().is_local() + || specifier.version().is_post() + { + return None; + } + Some(Self { inner: specifier }) + } + + /// Whether a patch version is present in this tilde version specifier. + pub fn has_patch(&self) -> bool { + self.inner.version.release().len() == 3 + } + + /// Construct the lower and upper bounding version specifiers for this tilde version specifier, + /// e.g., for `~=3.13` this would return `>=3.13` and `<4` and for `~=3.13.0` it would + /// return `>=3.13.0` and `<3.14`. + pub fn bounding_specifiers(&self) -> (VersionSpecifier, VersionSpecifier) { + let release = self.inner.version().release(); + let lower = self.inner.version.clone(); + let upper = if self.has_patch() { + Version::new([release[0], release[1] + 1]) + } else { + Version::new([release[0] + 1]) + }; + ( + VersionSpecifier::greater_than_equal_version(lower), + VersionSpecifier::less_than_version(upper), + ) + } + + /// Construct a new tilde `VersionSpecifier` with the given patch version appended. + pub fn with_patch_version(&self, patch: u64) -> TildeVersionSpecifier { + let mut release = self.inner.version.release().to_vec(); + if self.has_patch() { + release.pop(); + } + release.push(patch); + TildeVersionSpecifier::from_specifier( + VersionSpecifier::from_version(Operator::TildeEqual, Version::new(release)) + .expect("We should always derive a valid new version specifier"), + ) + .expect("We should always derive a new tilde version specifier") + } +} + +impl std::fmt::Display for TildeVersionSpecifier<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.inner) + } +} + #[cfg(test)] mod tests { use std::{cmp::Ordering, str::FromStr}; diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index 5ed6293e2..a768650d7 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -25,7 +25,7 @@ use uv_fs::{CWD, LockedFile, Simplified}; use uv_git::ResolvedRepositoryReference; use uv_installer::{SatisfiesResult, SitePackages}; use uv_normalize::{DEV_DEPENDENCIES, DefaultGroups, ExtraName, GroupName, PackageName}; -use uv_pep440::{Version, VersionSpecifiers}; +use uv_pep440::{TildeVersionSpecifier, Version, VersionSpecifiers}; use uv_pep508::MarkerTreeContents; use uv_pypi_types::{ConflictPackage, ConflictSet, Conflicts}; use uv_python::{ @@ -421,6 +421,30 @@ pub(crate) fn find_requires_python( if requires_python.is_empty() { return Ok(None); } + for ((package, group), specifiers) in &requires_python { + if let [spec] = &specifiers[..] { + if let Some(spec) = TildeVersionSpecifier::from_specifier_ref(spec) { + if spec.has_patch() { + continue; + } + let (lower, upper) = spec.bounding_specifiers(); + let spec_0 = spec.with_patch_version(0); + let (lower_0, upper_0) = spec_0.bounding_specifiers(); + warn_user_once!( + "The `requires-python` specifier (`{spec}`) in `{package}{group}` \ + uses the tilde specifier (`~=`) without a patch version. This will be \ + interpreted as `{lower}, {upper}`. Did you mean `{spec_0}` to constrain the \ + version as `{lower_0}, {upper_0}`? We recommend only using \ + the tilde specifier with a patch version to avoid ambiguity.", + group = if let Some(group) = group { + format!(":{group}") + } else { + String::new() + }, + ); + } + } + } match RequiresPython::intersection(requires_python.iter().map(|(.., specifiers)| specifiers)) { Some(requires_python) => Ok(Some(requires_python)), None => Err(ProjectError::DisjointRequiresPython(requires_python)), diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index 1c375b2e3..82754381b 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -4551,15 +4551,15 @@ fn lock_requires_python_compatible_specifier() -> Result<()> { "#, )?; - uv_snapshot!(context.filters(), context.lock(), @r###" + uv_snapshot!(context.filters(), context.lock(), @r" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- - warning: The release specifier (`~=3.13`) contains a compatible release match without a patch version. This will be interpreted as `>=3.13, <4`. Did you mean `~=3.13.0` to freeze the minor version? + warning: The `requires-python` specifier (`~=3.13`) in `warehouse` uses the tilde specifier (`~=`) without a patch version. This will be interpreted as `>=3.13, <4`. Did you mean `~=3.13.0` to constrain the version as `>=3.13.0, <3.14`? We recommend only using the tilde specifier with a patch version to avoid ambiguity. Resolved 1 package in [TIME] - "###); + "); pyproject_toml.write_str( r#" From a9ea756d141d935ab321a1aabdb78720d536073a Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 2 Jul 2025 11:11:51 -0400 Subject: [PATCH 160/185] Ignore Python patch version for `--universal` pip compile (#14405) ## Summary The idea here is that if a user runs `uv pip compile --universal`, we should ignore the patch version on the current interpreter. I think this makes sense... `--universal` tries to resolve for all future versions, so it seems a bit odd that we'd start at the _current_ patch version. Closes https://github.com/astral-sh/uv/issues/14397. --- crates/uv/src/commands/pip/compile.rs | 13 +++-- crates/uv/tests/it/pip_compile.rs | 76 +++++++++++++++++++++------ 2 files changed, 66 insertions(+), 23 deletions(-) diff --git a/crates/uv/src/commands/pip/compile.rs b/crates/uv/src/commands/pip/compile.rs index 3aa3fb0e9..a1846d418 100644 --- a/crates/uv/src/commands/pip/compile.rs +++ b/crates/uv/src/commands/pip/compile.rs @@ -337,13 +337,12 @@ pub(crate) async fn pip_compile( // Determine the Python requirement, if the user requested a specific version. let python_requirement = if universal { - let requires_python = RequiresPython::greater_than_equal_version( - if let Some(python_version) = python_version.as_ref() { - &python_version.version - } else { - interpreter.python_version() - }, - ); + let requires_python = if let Some(python_version) = python_version.as_ref() { + RequiresPython::greater_than_equal_version(&python_version.version) + } else { + let version = interpreter.python_minor_version(); + RequiresPython::greater_than_equal_version(&version) + }; PythonRequirement::from_requires_python(&interpreter, requires_python) } else if let Some(python_version) = python_version.as_ref() { PythonRequirement::from_python_version(&interpreter, python_version) diff --git a/crates/uv/tests/it/pip_compile.rs b/crates/uv/tests/it/pip_compile.rs index c80027761..ff135d959 100644 --- a/crates/uv/tests/it/pip_compile.rs +++ b/crates/uv/tests/it/pip_compile.rs @@ -16345,7 +16345,7 @@ fn pep_751_compile_registry_wheel() -> Result<()> { # uv pip compile --cache-dir [CACHE_DIR] requirements.txt --universal -o pylock.toml lock-version = "1.0" created-by = "uv" - requires-python = ">=3.12.[X]" + requires-python = ">=3.12" [[packages]] name = "iniconfig" @@ -16394,7 +16394,7 @@ fn pep_751_compile_registry_sdist() -> Result<()> { # uv pip compile --cache-dir [CACHE_DIR] requirements.txt --universal -o pylock.toml lock-version = "1.0" created-by = "uv" - requires-python = ">=3.12.[X]" + requires-python = ">=3.12" [[packages]] name = "source-distribution" @@ -16478,7 +16478,7 @@ fn pep_751_compile_directory() -> Result<()> { # uv pip compile --cache-dir [CACHE_DIR] requirements.txt --universal -o pylock.toml lock-version = "1.0" created-by = "uv" - requires-python = ">=3.12.[X]" + requires-python = ">=3.12" [[packages]] name = "anyio" @@ -16549,7 +16549,7 @@ fn pep_751_compile_git() -> Result<()> { # uv pip compile --cache-dir [CACHE_DIR] requirements.txt --universal -o pylock.toml lock-version = "1.0" created-by = "uv" - requires-python = ">=3.12.[X]" + requires-python = ">=3.12" [[packages]] name = "uv-public-pypackage" @@ -16599,7 +16599,7 @@ fn pep_751_compile_url_wheel() -> Result<()> { # uv pip compile --cache-dir [CACHE_DIR] requirements.txt --universal -o pylock.toml lock-version = "1.0" created-by = "uv" - requires-python = ">=3.12.[X]" + requires-python = ">=3.12" [[packages]] name = "anyio" @@ -16663,7 +16663,7 @@ fn pep_751_compile_url_sdist() -> Result<()> { # uv pip compile --cache-dir [CACHE_DIR] requirements.txt --universal -o pylock.toml lock-version = "1.0" created-by = "uv" - requires-python = ">=3.12.[X]" + requires-python = ">=3.12" [[packages]] name = "anyio" @@ -16732,7 +16732,7 @@ fn pep_751_compile_path_wheel() -> Result<()> { # uv pip compile --cache-dir [CACHE_DIR] requirements.txt --universal -o pylock.toml lock-version = "1.0" created-by = "uv" - requires-python = ">=3.12.[X]" + requires-python = ">=3.12" [[packages]] name = "iniconfig" @@ -16770,7 +16770,7 @@ fn pep_751_compile_path_wheel() -> Result<()> { # uv pip compile --cache-dir [CACHE_DIR] requirements.txt --universal -o nested/pylock.toml lock-version = "1.0" created-by = "uv" - requires-python = ">=3.12.[X]" + requires-python = ">=3.12" [[packages]] name = "iniconfig" @@ -16811,7 +16811,7 @@ fn pep_751_compile_path_sdist() -> Result<()> { # uv pip compile --cache-dir [CACHE_DIR] requirements.txt --universal -o pylock.toml lock-version = "1.0" created-by = "uv" - requires-python = ">=3.12.[X]" + requires-python = ">=3.12" [[packages]] name = "iniconfig" @@ -16850,7 +16850,7 @@ fn pep_751_compile_path_sdist() -> Result<()> { # uv pip compile --cache-dir [CACHE_DIR] requirements.txt --universal -o nested/pylock.toml lock-version = "1.0" created-by = "uv" - requires-python = ">=3.12.[X]" + requires-python = ">=3.12" [[packages]] name = "iniconfig" @@ -16887,7 +16887,7 @@ fn pep_751_compile_preferences() -> Result<()> { # uv pip compile --cache-dir [CACHE_DIR] requirements.txt --universal -o pylock.toml lock-version = "1.0" created-by = "uv" - requires-python = ">=3.12.[X]" + requires-python = ">=3.12" [[packages]] name = "anyio" @@ -16928,7 +16928,7 @@ fn pep_751_compile_preferences() -> Result<()> { # uv pip compile --cache-dir [CACHE_DIR] requirements.txt --universal -o pylock.toml lock-version = "1.0" created-by = "uv" - requires-python = ">=3.12.[X]" + requires-python = ">=3.12" [[packages]] name = "anyio" @@ -16968,7 +16968,7 @@ fn pep_751_compile_preferences() -> Result<()> { # uv pip compile --cache-dir [CACHE_DIR] requirements.txt --universal -o pylock.toml lock-version = "1.0" created-by = "uv" - requires-python = ">=3.12.[X]" + requires-python = ">=3.12" [[packages]] name = "anyio" @@ -17007,7 +17007,7 @@ fn pep_751_compile_preferences() -> Result<()> { # uv pip compile --cache-dir [CACHE_DIR] requirements.txt --universal -o pylock.toml lock-version = "1.0" created-by = "uv" - requires-python = ">=3.12.[X]" + requires-python = ">=3.12" [[packages]] name = "anyio" @@ -17055,7 +17055,7 @@ fn pep_751_compile_warn() -> Result<()> { # uv pip compile --cache-dir [CACHE_DIR] requirements.txt --universal -o pylock.toml --emit-index-url lock-version = "1.0" created-by = "uv" - requires-python = ">=3.12.[X]" + requires-python = ">=3.12" [[packages]] name = "iniconfig" @@ -17268,7 +17268,7 @@ fn pep_751_compile_no_emit_package() -> Result<()> { # uv pip compile --cache-dir [CACHE_DIR] requirements.txt --universal -o pylock.toml --no-emit-package idna lock-version = "1.0" created-by = "uv" - requires-python = ">=3.12.[X]" + requires-python = ">=3.12" [[packages]] name = "anyio" @@ -17562,3 +17562,47 @@ fn git_path_transitive_dependency() -> Result<()> { Ok(()) } + +/// Ensure that `--emit-index-annotation` plays nicely with `--annotation-style=line`. +#[test] +fn omit_python_patch_universal() -> Result<()> { + let context = TestContext::new("3.11"); + + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str("redis")?; + + uv_snapshot!(context.filters(), context.pip_compile() + .arg("requirements.in"), @r" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] requirements.in + redis==5.0.3 + # via -r requirements.in + + ----- stderr ----- + Resolved 1 package in [TIME] + " + ); + + uv_snapshot!(context.filters(), context.pip_compile() + .arg("requirements.in") + .arg("--universal"), @r" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] requirements.in --universal + async-timeout==4.0.3 ; python_full_version < '3.11.[X]' + # via redis + redis==5.0.3 + # via -r requirements.in + + ----- stderr ----- + Resolved 2 packages in [TIME] + " + ); + + Ok(()) +} From 2f53ea5c5c3f9939d87ab1aa75c5208a953532c1 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Wed, 2 Jul 2025 12:25:19 -0500 Subject: [PATCH 161/185] Add a migration guide from pip to uv projects (#12382) [Rendered](https://github.com/astral-sh/uv/blob/zb/pip-wip/docs/guides/migration/pip-to-project.md) --------- Co-authored-by: samypr100 <3933065+samypr100@users.noreply.github.com> Co-authored-by: Mathieu Kniewallner Co-authored-by: Aria Desires --- docs/guides/migration/index.md | 14 + docs/guides/migration/pip-to-project.md | 472 ++++++++++++++++++++++++ mkdocs.template.yml | 3 + 3 files changed, 489 insertions(+) create mode 100644 docs/guides/migration/index.md create mode 100644 docs/guides/migration/pip-to-project.md diff --git a/docs/guides/migration/index.md b/docs/guides/migration/index.md new file mode 100644 index 000000000..aa5f0f44f --- /dev/null +++ b/docs/guides/migration/index.md @@ -0,0 +1,14 @@ +# Migration guides + +Learn how to migrate from other tools to uv: + +- [Migrate from pip to uv projects](./pip-to-project.md) + +!!! note + + Other guides, such as migrating from another project management tool, or from pip to `uv pip` + are not yet available. See [#5200](https://github.com/astral-sh/uv/issues/5200) to track + progress. + +Or, explore the [integration guides](../integration/index.md) to learn how to use uv with other +software. diff --git a/docs/guides/migration/pip-to-project.md b/docs/guides/migration/pip-to-project.md new file mode 100644 index 000000000..a2be9ccd3 --- /dev/null +++ b/docs/guides/migration/pip-to-project.md @@ -0,0 +1,472 @@ +# Migrating from pip to a uv project + +This guide will discuss converting from a `pip` and `pip-tools` workflow centered on `requirements` +files to uv's project workflow using a `pyproject.toml` and `uv.lock` file. + +!!! note + + If you're looking to migrate from `pip` and `pip-tools` to uv's drop-in interface or from an + existing workflow where you're already using a `pyproject.toml`, those guides are not yet + written. See [#5200](https://github.com/astral-sh/uv/issues/5200) to track progress. + +We'll start with an overview of developing with `pip`, then discuss migrating to uv. + +!!! tip + + If you're familiar with the ecosystem, you can jump ahead to the + [requirements file import](#importing-requirements-files) instructions. + +## Understanding pip workflows + +### Project dependencies + +When you want to use a package in your project, you need to install it first. `pip` supports +imperative installation of packages, e.g.: + +```console +$ pip install fastapi +``` + +This installs the package into the environment that `pip` is installed in. This may be a virtual +environment, or, the global environment of your system's Python installation. + +Then, you can run a Python script that requires the package: + +```python title="example.py" +import fastapi +``` + +It's best practice to create a virtual environment for each project, to avoid mixing packages +between them. For example: + +```console +$ python -m venv +$ source .venv/bin/activate +$ pip ... +``` + +We will revisit this topic in the [project environments section](#project-environments) below. + +### Requirements files + +When sharing projects with others, it's useful to declare all the packages you require upfront. +`pip` supports installing requirements from a file, e.g.: + +```python title="requirements.txt" +fastapi +``` + +```console +$ pip install -r requirements.txt +``` + +Notice above that `fastapi` is not "locked" to a specific version — each person working on the +project may have a different version of `fastapi` installed. `pip-tools` was created to improve this +experience. + +When using `pip-tools`, requirements files specify both the dependencies for your project and lock +dependencies to a specific version — the file extension is used to differentiate between the two. +For example, if you require `fastapi` and `pydantic`, you'd specify these in a `requirements.in` +file: + +```python title="requirements.in" +fastapi +pydantic>2 +``` + +Notice there's a version constraint on `pydantic` — this means only `pydantic` versions later than +`2.0.0` can be used. In contrast, `fastapi` does not have a version constraint — any version can be +used. + +These dependencies can be compiled into a `requirements.txt` file: + +```console +$ pip-compile requirements.in -o requirements.txt +``` + +```python title="requirements.txt" +annotated-types==0.7.0 + # via pydantic +anyio==4.8.0 + # via starlette +fastapi==0.115.11 + # via -r requirements.in +idna==3.10 + # via anyio +pydantic==2.10.6 + # via + # -r requirements.in + # fastapi +pydantic-core==2.27.2 + # via pydantic +sniffio==1.3.1 + # via anyio +starlette==0.46.1 + # via fastapi +typing-extensions==4.12.2 + # via + # fastapi + # pydantic + # pydantic-core +``` + +Here, all the versions constraints are _exact_. Only a single version of each package can be used. +The above example was generated with `uv pip compile`, but could also be generated with +`pip-compile` from `pip-tools`. + +Though less common, the `requirements.txt` can also be generated using `pip freeze`, by first +installing the input dependencies into the environment then exporting the installed versions: + +```console +$ pip install -r requirements.in +$ pip freeze > requirements.txt +``` + +```python title="requirements.txt" +annotated-types==0.7.0 +anyio==4.8.0 +fastapi==0.115.11 +idna==3.10 +pydantic==2.10.6 +pydantic-core==2.27.2 +sniffio==1.3.1 +starlette==0.46.1 +typing-extensions==4.12.2 +``` + +After compiling dependencies into a locked set of versions, these files are committed to version +control and distributed with the project. + +Then, when someone wants to use the project, they install from the requirements file: + +```console +$ pip install -r requirements.txt +``` + + + +### Development dependencies + +The requirements file format can only describe a single set of dependencies at once. This means if +you have additional _groups_ of dependencies, such as development dependencies, they need separate +files. For example, we'll create a `-dev` dependency file: + +```python title="requirements-dev.in" +-r requirements.in +-c requirements.txt + +pytest +``` + +Notice the base requirements are included with `-r requirements.in`. This ensures your development +environment considers _all_ of the dependencies together. The `-c requirements.txt` _constrains_ the +package version to ensure that the `requirements-dev.txt` uses the same versions as +`requirements.txt`. + +!!! note + + It's common to use `-r requirements.txt` directly instead of using both + `-r requirements.in`, and `-c requirements.txt`. There's no difference in the resulting package + versions, but using both files produces annotations which allow you to determine which + dependencies are _direct_ (annotated with `-r requirements.in`) and which are _indirect_ (only + annotated with `-c requirements.txt`). + +The compiled development dependencies look like: + +```python title="requirements-dev.txt" +annotated-types==0.7.0 + # via + # -c requirements.txt + # pydantic +anyio==4.8.0 + # via + # -c requirements.txt + # starlette +fastapi==0.115.11 + # via + # -c requirements.txt + # -r requirements.in +idna==3.10 + # via + # -c requirements.txt + # anyio +iniconfig==2.0.0 + # via pytest +packaging==24.2 + # via pytest +pluggy==1.5.0 + # via pytest +pydantic==2.10.6 + # via + # -c requirements.txt + # -r requirements.in + # fastapi +pydantic-core==2.27.2 + # via + # -c requirements.txt + # pydantic +pytest==8.3.5 + # via -r requirements-dev.in +sniffio==1.3.1 + # via + # -c requirements.txt + # anyio +starlette==0.46.1 + # via + # -c requirements.txt + # fastapi +typing-extensions==4.12.2 + # via + # -c requirements.txt + # fastapi + # pydantic + # pydantic-core +``` + +As with the base dependency files, these are committed to version control and distributed with the +project. When someone wants to work on the project, they'll install from the requirements file: + +```console +$ pip install -r requirements-dev.txt +``` + +### Platform-specific dependencies + +When compiling dependencies with `pip` or `pip-tools`, the result is only usable on the same +platform as it is generated on. This poses a problem for projects which need to be usable on +multiple platforms, such as Windows and macOS. + +For example, take a simple dependency: + +```python title="requirements.in" +tqdm +``` + +On Linux, this compiles to: + +```python title="requirements-linux.txt" +tqdm==4.67.1 + # via -r requirements.in +``` + +While on Windows, this compiles to: + +```python title="requirements-win.txt" +colorama==0.4.6 + # via tqdm +tqdm==4.67.1 + # via -r requirements.in +``` + +`colorama` is a Windows-only dependency of `tqdm`. + +When using `pip` and `pip-tools`, a project needs to declare a requirements lock file for each +supported platform. + +!!! note + + uv's resolver can compile dependencies for multiple platforms at once (see ["universal resolution"](../../concepts/resolution.md#universal-resolution)), + allowing you to use a single `requirements.txt` for all platforms: + + ```console + $ uv pip compile --universal requirements.in + ``` + + ```python title="requirements.txt" + colorama==0.4.6 ; sys_platform == 'win32' + # via tqdm + tqdm==4.67.1 + # via -r requirements.in + ``` + + This resolution mode is also used when using a `pyproject.toml` and `uv.lock`. + +## Migrating to a uv project + +### The `pyproject.toml` + +The `pyproject.toml` is a standardized file for Python project metadata. It replaces +`requirements.in` files, allowing you to represent arbitrary groups of project dependencies. It also +provides a centralized location for metadata about your project, such as the build system or tool +settings. + + + +For example, the `requirements.in` and `requirements-dev.in` files above can be translated to a +`pyproject.toml` as follows: + +```toml title="pyproject.toml" +[project] +name = "example" +version = "0.0.1" +dependencies = [ + "fastapi", + "pydantic>2" +] + +[dependency-groups] +dev = ["pytest"] +``` + +We'll discuss the commands necessary to automate these imports below. + +### The uv lockfile + +uv uses a lockfile (`uv.lock`) file to lock package versions. The format of this file is specific to +uv, allowing uv to support advanced features. It replaces `requirements.txt` files. + +The lockfile will be automatically created and populated when adding dependencies, but you can +explicitly create it with `uv lock`. + +Unlike `requirements.txt` files, the `uv.lock` file can represent arbitrary groups of dependencies, +so multiple files are not needed to lock development dependencies. + +The uv lockfile is always [universal](../../concepts/resolution.md#universal-resolution), so +multiple files are not needed to +[lock dependencies for each platform](#platform-specific-dependencies). This ensures that all +developers + +The uv lockfile also supports concepts like +[pinning packages to specific indexes](../../concepts/indexes.md#pinning-a-package-to-an-index), +which is not representable in `requirements.txt` files. + +!!! tip + + If you only need to lock for a subset of platforms, use the + [`tool.uv.environments`](../../concepts/resolution.md#limited-resolution-environments) setting + to limit the resolution and lockfile. + +To learn more, see the [lockfile](../../concepts/projects/layout.md#the-lockfile) documentation. + +### Importing requirements files + +First, create a `pyproject.toml` if you have not already: + +```console +$ uv init +``` + +Then, the easiest way to import requirements is with `uv add`: + +```console +$ uv add -r requirements.in +``` + +However, there is some nuance to this transition. Notice we used the `requirements.in` file, which +does not pin to exact versions of packages so uv will solve for new versions of these packages. You +may want to continue using your previously locked versions from your `requirements.txt` so, when +switching over to uv, none of your dependency versions change. + +The solution is to add your locked versions as _constraints_. uv supports using these on `add` to +preserve locked versions: + +```console +$ uv add -r requirements.in -c requirements.txt +``` + +Your existing versions will be retained when producing a `uv.lock` file. + +#### Importing platform-specific constraints + +If your platform-specific dependencies have been compiled into separate files, you can still +transition to a universal lockfile. However, you cannot just use `-c` to specify constraints from +your existing platform-specific `requirements.txt` files because they do not include markers +describing the environment and will consequently conflict. + +To add the necessary markers, use `uv pip compile` to convert your existing files. For example, +given the following: + +```python title="requirements-win.txt" +colorama==0.4.6 + # via tqdm +tqdm==4.67.1 + # via -r requirements.in +``` + +The markers can be added with: + +```console +$ uv pip compile requirements.in -o requirements-win.txt --python-platform windows --no-strip-markers +``` + +Notice the resulting output includes a Windows marker on `colorama`: + +```python title="requirements-win.txt" +colorama==0.4.6 ; sys_platform == 'win32' + # via tqdm +tqdm==4.67.1 + # via -r requirements.in +``` + +When using `-o`, uv will constrain the versions to match the existing output file, if it can. + +Markers can be added for other platforms by changing the `--python-platform` and `-o` values for +each requirements file you need to import, e.g., to `linux` and `macos`. + +Once each `requirements.txt` file has been transformed, the dependencies can be imported to the +`pyproject.toml` and `uv.lock` with `uv add`: + +```console +$ uv add -r requirements.in -c requirements-win.txt -c requirements-linux.txt +``` + +#### Importing development dependency files + +As discussed in the [development dependencies](#development-dependencies) section, it's common to +have groups of dependencies for development purposes. + +To import development dependencies, use the `--dev` flag during `uv add`: + +```console +$ uv add --dev -r requirements-dev.in -c requirements-dev.txt +``` + +If the `requirements-dev.in` includes the parent `requirements.in` via `-r`, it will need to be +stripped to avoid adding the base requirements to the `dev` dependency group. The following example +uses `sed` to strip lines that start with `-r`, then pipes the result to `uv add`: + +```console +$ sed '/^-r /d' requirements-dev.in | uv add --dev -r - -c requirements-dev.txt +``` + +In addition to the `dev` dependency group, uv supports arbitrary group names. For example, if you +also have a dedicated set of dependencies for building your documentation, those can be imported to +a `docs` group: + +```console +$ uv add -r requirements-docs.in -c requirements-docs.txt --group docs +``` + +### Project environments + +Unlike `pip`, uv is not centered around the concept of an "active" virtual environment. Instead, uv +uses a dedicated virtual environment for each project in a `.venv` directory. This environment is +automatically managed, so when you run a command, like `uv add`, the environment is synced with the +project dependencies. + +The preferred way to execute commands in the environment is with `uv run`, e.g.: + +```console +$ uv run pytest +``` + +Prior to every `uv run` invocation, uv will verify that the lockfile is up-to-date with the +`pyproject.toml`, and that the environment is up-to-date with the lockfile, keeping your project +in-sync without the need for manual intervention. `uv run` guarantees that your command is run in a +consistent, locked environment. + +The project environment can also be explicitly created with `uv sync`, e.g., for use with editors. + +!!! note + + When in projects, uv will prefer a `.venv` in the project directory and ignore the active + environment as declared by the `VIRTUAL_ENV` variable by default. You can opt-in to using the + active environment with the `--active` flag. + +To learn more, see the +[project environment](../../concepts/projects/layout.md#the-project-environment) documentation. + +## Next steps + +Now that you've migrated to uv, take a look at the +[project concept](../../concepts/projects/index.md) page for more details about uv projects. diff --git a/mkdocs.template.yml b/mkdocs.template.yml index 0b2ee6623..69a299b5b 100644 --- a/mkdocs.template.yml +++ b/mkdocs.template.yml @@ -174,6 +174,9 @@ nav: - Using tools: guides/tools.md - Working on projects: guides/projects.md - Publishing packages: guides/package.md + - Migration: + - guides/migration/index.md + - From pip to a uv project: guides/migration/pip-to-project.md - Integrations: - guides/integration/index.md - Docker: guides/integration/docker.md From 743260b1f53e4ba11cc904ced2c717f509a7b0e2 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 2 Jul 2025 14:03:43 -0400 Subject: [PATCH 162/185] Make project and interpreter lock acquisition non-fatal (#14404) ## Summary If we fail to acquire a lock on an environment, uv shouldn't fail; we should just warn. In some cases, users run uv with read-only permissions for their projects, etc. For now, I kept any locks acquired _in the cache_ as hard failures, since we always need write-access to the cache. Closes https://github.com/astral-sh/uv/issues/14411. --- crates/uv-build-frontend/src/lib.rs | 10 +++-- crates/uv/src/commands/pip/install.rs | 10 ++++- crates/uv/src/commands/pip/sync.rs | 10 ++++- crates/uv/src/commands/pip/uninstall.rs | 10 ++++- crates/uv/src/commands/project/add.rs | 10 ++++- crates/uv/src/commands/project/mod.rs | 15 ++++++- crates/uv/src/commands/project/remove.rs | 10 ++++- crates/uv/src/commands/project/run.rs | 24 ++++++++-- crates/uv/src/commands/project/sync.rs | 9 +++- crates/uv/tests/it/sync.rs | 56 +++++++++++++++++++++++- 10 files changed, 143 insertions(+), 21 deletions(-) diff --git a/crates/uv-build-frontend/src/lib.rs b/crates/uv-build-frontend/src/lib.rs index 06c07425c..5cbaece2e 100644 --- a/crates/uv-build-frontend/src/lib.rs +++ b/crates/uv-build-frontend/src/lib.rs @@ -25,7 +25,7 @@ use tempfile::TempDir; use tokio::io::AsyncBufReadExt; use tokio::process::Command; use tokio::sync::{Mutex, Semaphore}; -use tracing::{Instrument, debug, info_span, instrument}; +use tracing::{Instrument, debug, info_span, instrument, warn}; use uv_cache_key::cache_digest; use uv_configuration::PreviewMode; @@ -456,8 +456,12 @@ impl SourceBuild { "uv-setuptools-{}.lock", cache_digest(&canonical_source_path) )); - source_tree_lock = - Some(LockedFile::acquire(lock_path, self.source_tree.to_string_lossy()).await?); + source_tree_lock = LockedFile::acquire(lock_path, self.source_tree.to_string_lossy()) + .await + .inspect_err(|err| { + warn!("Failed to acquire build lock: {err}"); + }) + .ok(); } Ok(source_tree_lock) } diff --git a/crates/uv/src/commands/pip/install.rs b/crates/uv/src/commands/pip/install.rs index b7d32dd94..aa6e6a6c9 100644 --- a/crates/uv/src/commands/pip/install.rs +++ b/crates/uv/src/commands/pip/install.rs @@ -5,7 +5,7 @@ use std::path::PathBuf; use anyhow::Context; use itertools::Itertools; use owo_colors::OwoColorize; -use tracing::{Level, debug, enabled}; +use tracing::{Level, debug, enabled, warn}; use uv_cache::Cache; use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder}; @@ -236,7 +236,13 @@ pub(crate) async fn pip_install( } } - let _lock = environment.lock().await?; + let _lock = environment + .lock() + .await + .inspect_err(|err| { + warn!("Failed to acquire environment lock: {err}"); + }) + .ok(); // Determine the markers to use for the resolution. let interpreter = environment.interpreter(); diff --git a/crates/uv/src/commands/pip/sync.rs b/crates/uv/src/commands/pip/sync.rs index ab4c42ce5..8f26aaea2 100644 --- a/crates/uv/src/commands/pip/sync.rs +++ b/crates/uv/src/commands/pip/sync.rs @@ -3,7 +3,7 @@ use std::fmt::Write; use anyhow::{Context, Result}; use owo_colors::OwoColorize; -use tracing::debug; +use tracing::{debug, warn}; use uv_cache::Cache; use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder}; @@ -211,7 +211,13 @@ pub(crate) async fn pip_sync( } } - let _lock = environment.lock().await?; + let _lock = environment + .lock() + .await + .inspect_err(|err| { + warn!("Failed to acquire environment lock: {err}"); + }) + .ok(); let interpreter = environment.interpreter(); diff --git a/crates/uv/src/commands/pip/uninstall.rs b/crates/uv/src/commands/pip/uninstall.rs index 4424fee37..835e7de65 100644 --- a/crates/uv/src/commands/pip/uninstall.rs +++ b/crates/uv/src/commands/pip/uninstall.rs @@ -3,7 +3,7 @@ use std::fmt::Write; use anyhow::Result; use itertools::{Either, Itertools}; use owo_colors::OwoColorize; -use tracing::debug; +use tracing::{debug, warn}; use uv_cache::Cache; use uv_client::BaseClientBuilder; @@ -100,7 +100,13 @@ pub(crate) async fn pip_uninstall( } } - let _lock = environment.lock().await?; + let _lock = environment + .lock() + .await + .inspect_err(|err| { + warn!("Failed to acquire environment lock: {err}"); + }) + .ok(); // Index the current `site-packages` directory. let site_packages = uv_installer::SitePackages::from_environment(&environment)?; diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index 719821df5..04fd7d822 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -10,7 +10,7 @@ use anyhow::{Context, Result, bail}; use itertools::Itertools; use owo_colors::OwoColorize; use rustc_hash::{FxBuildHasher, FxHashMap}; -use tracing::debug; +use tracing::{debug, warn}; use url::Url; use uv_cache::Cache; @@ -319,7 +319,13 @@ pub(crate) async fn add( } }; - let _lock = target.acquire_lock().await?; + let _lock = target + .acquire_lock() + .await + .inspect_err(|err| { + warn!("Failed to acquire environment lock: {err}"); + }) + .ok(); let client_builder = BaseClientBuilder::new() .connectivity(network_settings.connectivity) diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index a768650d7..c327e8a44 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -1244,7 +1244,12 @@ impl ProjectEnvironment { preview: PreviewMode, ) -> Result { // Lock the project environment to avoid synchronization issues. - let _lock = ProjectInterpreter::lock(workspace).await?; + let _lock = ProjectInterpreter::lock(workspace) + .await + .inspect_err(|err| { + warn!("Failed to acquire project environment lock: {err}"); + }) + .ok(); let upgradeable = preview.is_enabled() && python @@ -1462,7 +1467,13 @@ impl ScriptEnvironment { preview: PreviewMode, ) -> Result { // Lock the script environment to avoid synchronization issues. - let _lock = ScriptInterpreter::lock(script).await?; + let _lock = ScriptInterpreter::lock(script) + .await + .inspect_err(|err| { + warn!("Failed to acquire script environment lock: {err}"); + }) + .ok(); + let upgradeable = python_request .as_ref() .is_none_or(|request| !request.includes_patch()); diff --git a/crates/uv/src/commands/project/remove.rs b/crates/uv/src/commands/project/remove.rs index 29b5f0bc0..6bc04160e 100644 --- a/crates/uv/src/commands/project/remove.rs +++ b/crates/uv/src/commands/project/remove.rs @@ -5,7 +5,7 @@ use std::str::FromStr; use anyhow::{Context, Result}; use owo_colors::OwoColorize; -use tracing::debug; +use tracing::{debug, warn}; use uv_cache::Cache; use uv_configuration::{ @@ -281,7 +281,13 @@ pub(crate) async fn remove( } }; - let _lock = target.acquire_lock().await?; + let _lock = target + .acquire_lock() + .await + .inspect_err(|err| { + warn!("Failed to acquire environment lock: {err}"); + }) + .ok(); // Determine the lock mode. let mode = if locked { diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index 6ece28eaf..a6ea4c0e0 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -240,7 +240,13 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl .await? .into_environment()?; - let _lock = environment.lock().await?; + let _lock = environment + .lock() + .await + .inspect_err(|err| { + warn!("Failed to acquire environment lock: {err}"); + }) + .ok(); // Determine the lock mode. let mode = if frozen { @@ -386,7 +392,13 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl ) }); - let _lock = environment.lock().await?; + let _lock = environment + .lock() + .await + .inspect_err(|err| { + warn!("Failed to acquire environment lock: {err}"); + }) + .ok(); match update_environment( environment, @@ -699,7 +711,13 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl .map(|lock| (lock, project.workspace().install_path().to_owned())); } } else { - let _lock = venv.lock().await?; + let _lock = venv + .lock() + .await + .inspect_err(|err| { + warn!("Failed to acquire environment lock: {err}"); + }) + .ok(); // Determine the lock mode. let mode = if frozen { diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index dc9f0dcbb..6e057446e 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -6,6 +6,7 @@ use std::sync::Arc; use anyhow::{Context, Result}; use itertools::Itertools; use owo_colors::OwoColorize; +use tracing::warn; use uv_cache::Cache; use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder}; @@ -169,7 +170,13 @@ pub(crate) async fn sync( ), }; - let _lock = environment.lock().await?; + let _lock = environment + .lock() + .await + .inspect_err(|err| { + warn!("Failed to acquire environment lock: {err}"); + }) + .ok(); // Notify the user of any environment changes. match &environment { diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index da59682ab..a97775d9f 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -3,13 +3,14 @@ use assert_cmd::prelude::*; use assert_fs::{fixture::ChildPath, prelude::*}; use indoc::{formatdoc, indoc}; use insta::assert_snapshot; - -use crate::common::{TestContext, download_to_disk, packse_index_url, uv_snapshot, venv_bin_path}; use predicates::prelude::predicate; use tempfile::tempdir_in; + use uv_fs::Simplified; use uv_static::EnvVars; +use crate::common::{TestContext, download_to_disk, packse_index_url, uv_snapshot, venv_bin_path}; + #[test] fn sync() -> Result<()> { let context = TestContext::new("3.12"); @@ -9989,3 +9990,54 @@ fn sync_url_with_query_parameters() -> Result<()> { Ok(()) } + +#[test] +#[cfg(unix)] +fn read_only() -> Result<()> { + use std::os::unix::fs::PermissionsExt; + + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["iniconfig"] + "#, + )?; + + uv_snapshot!(context.filters(), context.sync(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + iniconfig==2.0.0 + "###); + + assert!(context.temp_dir.child("uv.lock").exists()); + + // Remove the flock. + fs_err::remove_file(context.venv.child(".lock"))?; + + // Make the virtual environment read and execute (but not write). + fs_err::set_permissions(&context.venv, std::fs::Permissions::from_mode(0o555))?; + + uv_snapshot!(context.filters(), context.sync(), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + Audited 1 package in [TIME] + "); + + Ok(()) +} From a6bb65c78deaf6fef7b901d4d24d350368db2723 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Wed, 2 Jul 2025 13:11:17 -0500 Subject: [PATCH 163/185] Clarify behavior and hint on tool install when no executables are available (#14423) Closes https://github.com/astral-sh/uv/issues/14416 --- crates/uv/src/commands/tool/common.rs | 12 ++++++++---- crates/uv/tests/it/tool_install.rs | 22 +++++++++++----------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/crates/uv/src/commands/tool/common.rs b/crates/uv/src/commands/tool/common.rs index 166b4fc6f..ffc1b5645 100644 --- a/crates/uv/src/commands/tool/common.rs +++ b/crates/uv/src/commands/tool/common.rs @@ -218,7 +218,7 @@ pub(crate) fn finalize_tool_install( if target_entry_points.is_empty() { writeln!( printer.stdout(), - "No executables are provided by `{from}`", + "No executables are provided by package `{from}`; removing tool", from = name.cyan() )?; @@ -354,7 +354,9 @@ fn hint_executable_from_dependency( let command = format!("uv tool install {}", package.name()); writeln!( printer.stdout(), - "However, an executable with the name `{}` is available via dependency `{}`.\nDid you mean `{}`?", + "{}{} An executable with the name `{}` is available via dependency `{}`.\n Did you mean `{}`?", + "hint".bold().cyan(), + ":".bold(), name.cyan(), package.name().cyan(), command.bold(), @@ -363,7 +365,9 @@ fn hint_executable_from_dependency( packages => { writeln!( printer.stdout(), - "However, an executable with the name `{}` is available via the following dependencies::", + "{}{} An executable with the name `{}` is available via the following dependencies::", + "hint".bold().cyan(), + ":".bold(), name.cyan(), )?; @@ -372,7 +376,7 @@ fn hint_executable_from_dependency( } writeln!( printer.stdout(), - "Did you mean to install one of them instead?" + " Did you mean to install one of them instead?" )?; } } diff --git a/crates/uv/tests/it/tool_install.rs b/crates/uv/tests/it/tool_install.rs index 0da627552..deb935f9b 100644 --- a/crates/uv/tests/it/tool_install.rs +++ b/crates/uv/tests/it/tool_install.rs @@ -448,13 +448,13 @@ fn tool_install_suggest_other_packages_with_executable() { uv_snapshot!(filters, context.tool_install() .arg("fastapi==0.111.0") .env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str()) - .env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()), @r###" + .env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()), @r" success: false exit_code: 1 ----- stdout ----- - No executables are provided by `fastapi` - However, an executable with the name `fastapi` is available via dependency `fastapi-cli`. - Did you mean `uv tool install fastapi-cli`? + No executables are provided by package `fastapi`; removing tool + hint: An executable with the name `fastapi` is available via dependency `fastapi-cli`. + Did you mean `uv tool install fastapi-cli`? ----- stderr ----- Resolved 35 packages in [TIME] @@ -494,7 +494,7 @@ fn tool_install_suggest_other_packages_with_executable() { + uvicorn==0.29.0 + watchfiles==0.21.0 + websockets==12.0 - "###); + "); } /// Test installing a tool at a version @@ -821,11 +821,11 @@ fn tool_install_remove_on_empty() -> Result<()> { .arg(black.path()) .env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str()) .env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()) - .env(EnvVars::PATH, bin_dir.as_os_str()), @r###" + .env(EnvVars::PATH, bin_dir.as_os_str()), @r" success: false exit_code: 1 ----- stdout ----- - No executables are provided by `black` + No executables are provided by package `black`; removing tool ----- stderr ----- Resolved 1 package in [TIME] @@ -839,7 +839,7 @@ fn tool_install_remove_on_empty() -> Result<()> { - packaging==24.0 - pathspec==0.12.1 - platformdirs==4.2.0 - "###); + "); // Re-request `black`. It should reinstall, without requiring `--force`. uv_snapshot!(context.filters(), context.tool_install() @@ -1649,18 +1649,18 @@ fn tool_install_no_entrypoints() { .arg("iniconfig") .env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str()) .env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()) - .env(EnvVars::PATH, bin_dir.as_os_str()), @r###" + .env(EnvVars::PATH, bin_dir.as_os_str()), @r" success: false exit_code: 1 ----- stdout ----- - No executables are provided by `iniconfig` + No executables are provided by package `iniconfig`; removing tool ----- stderr ----- Resolved 1 package in [TIME] Prepared 1 package in [TIME] Installed 1 package in [TIME] + iniconfig==2.0.0 - "###); + "); // Ensure the tool environment is not created. tool_dir From ec54dce9191c8cde770572afde78ab4ce4001f6a Mon Sep 17 00:00:00 2001 From: Jack O'Connor Date: Wed, 2 Jul 2025 12:40:18 -0700 Subject: [PATCH 164/185] Includes `sys.prefix` in cached environment keys to avoid `--with` collisions across projects (#14403) Fixes https://github.com/astral-sh/uv/issues/12889. --------- Co-authored-by: Zanie Blue --- crates/uv/src/commands/project/environment.rs | 39 +++++++++++++++---- crates/uv/tests/it/run.rs | 22 +++++++---- 2 files changed, 45 insertions(+), 16 deletions(-) diff --git a/crates/uv/src/commands/project/environment.rs b/crates/uv/src/commands/project/environment.rs index f5a9713d2..f43587ff0 100644 --- a/crates/uv/src/commands/project/environment.rs +++ b/crates/uv/src/commands/project/environment.rs @@ -44,13 +44,15 @@ impl CachedEnvironment { printer: Printer, preview: PreviewMode, ) -> Result { - let interpreter = Self::base_interpreter(interpreter, cache)?; + // Resolve the "base" interpreter, which resolves to an underlying parent interpreter if the + // given interpreter is a virtual environment. + let base_interpreter = Self::base_interpreter(interpreter, cache)?; // Resolve the requirements with the interpreter. let resolution = Resolution::from( resolve_environment( spec, - &interpreter, + &base_interpreter, build_constraints.clone(), &settings.resolver, network_settings, @@ -73,13 +75,34 @@ impl CachedEnvironment { hash_digest(&distributions) }; - // Hash the interpreter based on its path. - // TODO(charlie): Come up with a robust hash for the interpreter. - let interpreter_hash = - cache_digest(&canonicalize_executable(interpreter.sys_executable())?); + // Construct a hash for the environment. + // + // Use the canonicalized base interpreter path since that's the interpreter we performed the + // resolution with and the interpreter the environment will be created with. + // + // We also include the canonicalized `sys.prefix` of the non-base interpreter, that is, the + // virtual environment's path. Originally, we shared cached environments independent of the + // environment they'd be layered on top of. However, this causes collisions as the overlay + // `.pth` file can be overridden by another instance of uv. Including this element in the key + // avoids this problem at the cost of creating separate cached environments for identical + // `--with` invocations across projects. We use `sys.prefix` rather than `sys.executable` so + // we can canonicalize it without invalidating the purpose of the element — it'd probably be + // safe to just use the absolute `sys.executable` as well. + // + // TODO(zanieb): Since we're not sharing these environmments across projects, we should move + // [`CachedEvnvironment::set_overlay`] etc. here since the values there should be constant + // now. + // + // TODO(zanieb): We should include the version of the base interpreter in the hash, so if + // the interpreter at the canonicalized path changes versions we construct a new + // environment. + let environment_hash = cache_digest(&( + &canonicalize_executable(base_interpreter.sys_executable())?, + &interpreter.sys_prefix().canonicalize()?, + )); // Search in the content-addressed cache. - let cache_entry = cache.entry(CacheBucket::Environments, interpreter_hash, resolution_hash); + let cache_entry = cache.entry(CacheBucket::Environments, environment_hash, resolution_hash); if cache.refresh().is_none() { if let Ok(root) = cache.resolve_link(cache_entry.path()) { @@ -93,7 +116,7 @@ impl CachedEnvironment { let temp_dir = cache.venv_dir()?; let venv = uv_virtualenv::create_venv( temp_dir.path(), - interpreter, + base_interpreter, uv_virtualenv::Prompt::None, false, false, diff --git a/crates/uv/tests/it/run.rs b/crates/uv/tests/it/run.rs index 82f3c2b0b..98c2adbfe 100644 --- a/crates/uv/tests/it/run.rs +++ b/crates/uv/tests/it/run.rs @@ -4777,7 +4777,7 @@ fn run_groups_include_requires_python() -> Result<()> { bar = ["iniconfig"] baz = ["iniconfig"] dev = ["sniffio", {include-group = "foo"}, {include-group = "baz"}] - + [tool.uv.dependency-groups] foo = {requires-python="<3.13"} @@ -4876,7 +4876,7 @@ fn exit_status_signal() -> Result<()> { #[test] fn run_repeated() -> Result<()> { - let context = TestContext::new_with_versions(&["3.13"]); + let context = TestContext::new_with_versions(&["3.13", "3.12"]); let pyproject_toml = context.temp_dir.child("pyproject.toml"); pyproject_toml.write_str(indoc! { r#" @@ -4923,22 +4923,25 @@ fn run_repeated() -> Result<()> { Resolved 1 package in [TIME] "###); - // Re-running as a tool shouldn't require reinstalling `typing-extensions`, since the environment is cached. + // Re-running as a tool does require reinstalling `typing-extensions`, since the base venv is + // different. uv_snapshot!( context.filters(), - context.tool_run().arg("--with").arg("typing-extensions").arg("python").arg("-c").arg("import typing_extensions; import iniconfig"), @r###" + context.tool_run().arg("--with").arg("typing-extensions").arg("python").arg("-c").arg("import typing_extensions; import iniconfig"), @r#" success: false exit_code: 1 ----- stdout ----- ----- stderr ----- Resolved 1 package in [TIME] + Installed 1 package in [TIME] + + typing-extensions==4.10.0 Traceback (most recent call last): File "", line 1, in import typing_extensions; import iniconfig ^^^^^^^^^^^^^^^^ ModuleNotFoundError: No module named 'iniconfig' - "###); + "#); Ok(()) } @@ -4979,22 +4982,25 @@ fn run_without_overlay() -> Result<()> { + typing-extensions==4.10.0 "###); - // Import `iniconfig` in the context of a `tool run` command, which should fail. + // Import `iniconfig` in the context of a `tool run` command, which should fail. Note that + // typing-extensions gets installed again, because the venv is not shared. uv_snapshot!( context.filters(), - context.tool_run().arg("--with").arg("typing-extensions").arg("python").arg("-c").arg("import typing_extensions; import iniconfig"), @r###" + context.tool_run().arg("--with").arg("typing-extensions").arg("python").arg("-c").arg("import typing_extensions; import iniconfig"), @r#" success: false exit_code: 1 ----- stdout ----- ----- stderr ----- Resolved 1 package in [TIME] + Installed 1 package in [TIME] + + typing-extensions==4.10.0 Traceback (most recent call last): File "", line 1, in import typing_extensions; import iniconfig ^^^^^^^^^^^^^^^^ ModuleNotFoundError: No module named 'iniconfig' - "###); + "#); // Re-running in the context of the project should reset the overlay. uv_snapshot!( From 3bb8ac610ca5690fce0eea9da792dbe6eecaad05 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 2 Jul 2025 14:51:17 -0500 Subject: [PATCH 165/185] Sync latest Python releases (#14426) Automated update for Python releases. Co-authored-by: zanieb <2586601+zanieb@users.noreply.github.com> --- .../uv-dev/src/generate_sysconfig_mappings.rs | 4 +- crates/uv-python/download-metadata.json | 936 +++++++++--------- .../src/sysconfig/generated_mappings.rs | 8 +- crates/uv-python/src/sysconfig/mod.rs | 2 +- 4 files changed, 472 insertions(+), 478 deletions(-) diff --git a/crates/uv-dev/src/generate_sysconfig_mappings.rs b/crates/uv-dev/src/generate_sysconfig_mappings.rs index e35d2c3fc..f556922c6 100644 --- a/crates/uv-dev/src/generate_sysconfig_mappings.rs +++ b/crates/uv-dev/src/generate_sysconfig_mappings.rs @@ -11,7 +11,7 @@ use crate::ROOT_DIR; use crate::generate_all::Mode; /// Contains current supported targets -const TARGETS_YML_URL: &str = "https://raw.githubusercontent.com/astral-sh/python-build-standalone/refs/tags/20250630/cpython-unix/targets.yml"; +const TARGETS_YML_URL: &str = "https://raw.githubusercontent.com/astral-sh/python-build-standalone/refs/tags/20250702/cpython-unix/targets.yml"; #[derive(clap::Args)] pub(crate) struct Args { @@ -130,7 +130,7 @@ async fn generate() -> Result { output.push_str("//! DO NOT EDIT\n"); output.push_str("//!\n"); output.push_str("//! Generated with `cargo run dev generate-sysconfig-metadata`\n"); - output.push_str("//! Targets from \n"); + output.push_str("//! Targets from \n"); output.push_str("//!\n"); // Disable clippy/fmt diff --git a/crates/uv-python/download-metadata.json b/crates/uv-python/download-metadata.json index 4035a6a48..b697da9c8 100644 --- a/crates/uv-python/download-metadata.json +++ b/crates/uv-python/download-metadata.json @@ -11,8 +11,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-aarch64-apple-darwin-install_only_stripped.tar.gz", - "sha256": "56f9d76823f75e6853d23dc893dbe9c5e8b4d120ba1e568ceb62f7aff7285625", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.14.0b3%2B20250702-aarch64-apple-darwin-install_only_stripped.tar.gz", + "sha256": "0b948f37363193fcf5e20c2e887183467907f1b6d04420fc5a0c0c7c421e7b12", "variant": null }, "cpython-3.14.0b3-darwin-x86_64-none": { @@ -27,8 +27,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-x86_64-apple-darwin-install_only_stripped.tar.gz", - "sha256": "135c7119d5ce56c97171769f51fee3be0e6ebfdab13c34dc28af46dbf00dffd1", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.14.0b3%2B20250702-x86_64-apple-darwin-install_only_stripped.tar.gz", + "sha256": "47f21cf35481e5ba8e4e6b35c4dd549b0463d0f1dc24134d6e7fcc832a292869", "variant": null }, "cpython-3.14.0b3-linux-aarch64-gnu": { @@ -43,8 +43,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "046cfd0b5132d1ac441eaa81d20d4e71f113c0f4ca1831be35fc2581171a2e47", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.14.0b3%2B20250702-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "2935079dd417d8940955f0b083be698ae27a1d65f947614c36ce5e4ea509c812", "variant": null }, "cpython-3.14.0b3-linux-armv7-gnueabi": { @@ -59,8 +59,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-armv7-unknown-linux-gnueabi-install_only_stripped.tar.gz", - "sha256": "7922cceb24b1c7bbff5da8fa663ea3d75f0622cca862806d7e06080051d87919", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.14.0b3%2B20250702-armv7-unknown-linux-gnueabi-install_only_stripped.tar.gz", + "sha256": "b64dfec2a7016ae5fa5340298f46c05df0c93a30021c009fd3db9b97a5cad92b", "variant": null }, "cpython-3.14.0b3-linux-armv7-gnueabihf": { @@ -75,8 +75,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-armv7-unknown-linux-gnueabihf-install_only_stripped.tar.gz", - "sha256": "4b3450a8ae7f19c682563f44105f9af946bd5504f585fc135f78b066c11290b4", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.14.0b3%2B20250702-armv7-unknown-linux-gnueabihf-install_only_stripped.tar.gz", + "sha256": "7139f66c73f09f8ed3fcd840e08b85dc591fe8df048cfa5c48dc695a68f74149", "variant": null }, "cpython-3.14.0b3-linux-powerpc64le-gnu": { @@ -91,8 +91,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-ppc64le-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "34c66ea418df6bff2f144a8317a8940827bf5545bb41e21ebf4ae991bc0d0fa7", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.14.0b3%2B20250702-ppc64le-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "5210b912d9dc1e7ee9fc215972c7c254ddaf9d64ad293f42af1a819896a4cbed", "variant": null }, "cpython-3.14.0b3-linux-riscv64-gnu": { @@ -107,8 +107,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-riscv64-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "952a785c85ae9b19ef5933bc406ed2ae622f535862f31f1d9d83579ebd7f1ab5", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.14.0b3%2B20250702-riscv64-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "66a8f6825c5e1b289bfd62370b4cc6c9b5212a91b0440dcf5408c4e3bcfcdddd", "variant": null }, "cpython-3.14.0b3-linux-s390x-gnu": { @@ -123,8 +123,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-s390x-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "09a6f67c1c8c1a056ac873c99b8dc9b393013fc06bee46b9a4f841686ac39948", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.14.0b3%2B20250702-s390x-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "2af3a5d27e7fd49b5796a35c1f4a17848d9e5d40c946b9e690d7c27e527d99d8", "variant": null }, "cpython-3.14.0b3-linux-x86_64-gnu": { @@ -139,8 +139,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "771f78fb170b706c769742712143c6e3d0ed23e6b906bfd6e0bd7e5f253a8c15", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.14.0b3%2B20250702-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "17643efc55b6b68b4fa7b3a5e43abb0ea31b4f03942e2d17bd04c5cd5be52c52", "variant": null }, "cpython-3.14.0b3-linux-x86_64-musl": { @@ -155,8 +155,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-x86_64-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "b5d645032acef89e5a112746bd0d7db5da2c04c72eebb5b7ca2dcd627c66502f", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.14.0b3%2B20250702-x86_64-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "1c35d7e5ac357d012d3c265da406e331535bf9fa5e29454b190ac8cc0c57dd40", "variant": null }, "cpython-3.14.0b3-linux-x86_64_v2-gnu": { @@ -171,8 +171,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-x86_64_v2-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "bba4f53b8f9d93957bacecceb70e39199fe235836d5f7898f10c061fe4f21a19", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.14.0b3%2B20250702-x86_64_v2-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "8d7c283a6f9e18377776968c5d5fcce9ff0a9c833c4f6c64d8f804da743e0e9d", "variant": null }, "cpython-3.14.0b3-linux-x86_64_v2-musl": { @@ -187,8 +187,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-x86_64_v2-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "7692fedeff8c378c2fd046204296d4e9c921151176bcae80bff51d9cff6217d6", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.14.0b3%2B20250702-x86_64_v2-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "75d5b65bae7b39f3e35a30070a7ccef0c773b1976e764c7fb68ba840a3ad0594", "variant": null }, "cpython-3.14.0b3-linux-x86_64_v3-gnu": { @@ -203,8 +203,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-x86_64_v3-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "7d220528664f7b86555fed946ce29e9522db63b49f7c952c7df9bdee0a002466", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.14.0b3%2B20250702-x86_64_v3-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "db25121d9a35f1613e281ead33903a7e6489d0506207451ef49d82eb71d722df", "variant": null }, "cpython-3.14.0b3-linux-x86_64_v3-musl": { @@ -219,8 +219,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-x86_64_v3-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "8188269487389fe6b1c441593257e5bd8bf6b8877a27ea0388d1a0282a2206f9", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.14.0b3%2B20250702-x86_64_v3-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "31cbe24575231d706937802a8f81536d11dd79f8c9cd7981b8f93b970a8e8481", "variant": null }, "cpython-3.14.0b3-linux-x86_64_v4-gnu": { @@ -235,8 +235,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-x86_64_v4-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "da2699921d0b1af5527fbe7484534a5afbc40bfd50b2b1d4c296ced849ae3c3f", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.14.0b3%2B20250702-x86_64_v4-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "3c98b94dfc77c9d6369c3cdc09e03abc0dad2ead2f40a6b52d1b119bdcb33ab7", "variant": null }, "cpython-3.14.0b3-linux-x86_64_v4-musl": { @@ -251,8 +251,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-x86_64_v4-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "059cab36f0a7fc71d3bf401ba73b41dc2df40b8f593e46c983cc5f2cd531ad19", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.14.0b3%2B20250702-x86_64_v4-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "0742eb6b381fdb6b57983f8a5918dd9e154953f959f2be5a203699e5b1901c1b", "variant": null }, "cpython-3.14.0b3-windows-aarch64-none": { @@ -267,8 +267,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-aarch64-pc-windows-msvc-install_only_stripped.tar.gz", - "sha256": "fa9c906275b89bf2068945394edb75be580ba8a3a868d647bcde2a3b958cf7ad", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.14.0b3%2B20250702-aarch64-pc-windows-msvc-install_only_stripped.tar.gz", + "sha256": "62dc6ff21cbbf2c216f1b9f573ed8e0433c0f7185280a13b2b2f3a81ac862b90", "variant": null }, "cpython-3.14.0b3-windows-i686-none": { @@ -283,8 +283,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-i686-pc-windows-msvc-install_only_stripped.tar.gz", - "sha256": "e1d5ae8b19492bd7520dd5d377740afcfebe222b5dc52452a69b06678df02deb", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.14.0b3%2B20250702-i686-pc-windows-msvc-install_only_stripped.tar.gz", + "sha256": "0fc98664753360e23eaf3aa169657627ca5766141a49e1cfb0397895cbb47826", "variant": null }, "cpython-3.14.0b3-windows-x86_64-none": { @@ -299,8 +299,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-x86_64-pc-windows-msvc-install_only_stripped.tar.gz", - "sha256": "d01bb3382f9d4e7e956914df3c9efef6817aaf8915afa3a51f965a41d32bf8de", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.14.0b3%2B20250702-x86_64-pc-windows-msvc-install_only_stripped.tar.gz", + "sha256": "5b5ef4c03b4e2aaab389f10b973914780d76bd82eeaeb3c305239a57aba2e367", "variant": null }, "cpython-3.14.0b3+freethreaded-darwin-aarch64-none": { @@ -315,8 +315,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-aarch64-apple-darwin-freethreaded%2Bpgo%2Blto-full.tar.zst", - "sha256": "475280f8fe3882e89cc4fa944a74dfaa5e4cfa5303b40803f4d782a08bf983a3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.14.0b3%2B20250702-aarch64-apple-darwin-freethreaded%2Bpgo%2Blto-full.tar.zst", + "sha256": "d19213021f5fd039d7021ccb41698cc99ca313064d7c1cc9b5ef8f831abb9961", "variant": "freethreaded" }, "cpython-3.14.0b3+freethreaded-darwin-x86_64-none": { @@ -331,8 +331,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-x86_64-apple-darwin-freethreaded%2Bpgo%2Blto-full.tar.zst", - "sha256": "79f93e3f673400764f00f5eb5f9d7c7c8ed20889a9a05f068b1cdc45141821c5", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.14.0b3%2B20250702-x86_64-apple-darwin-freethreaded%2Bpgo%2Blto-full.tar.zst", + "sha256": "26ec6697bbb38c3fa6275e79e110854b2585914ca503c65916478e7ca8d0491b", "variant": "freethreaded" }, "cpython-3.14.0b3+freethreaded-linux-aarch64-gnu": { @@ -347,8 +347,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-aarch64-unknown-linux-gnu-freethreaded%2Blto-full.tar.zst", - "sha256": "14ea3ce113f8eff97282c917c29dda2b2e9d9add07100b0e9b48377f1b5691cf", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.14.0b3%2B20250702-aarch64-unknown-linux-gnu-freethreaded%2Bpgo%2Blto-full.tar.zst", + "sha256": "b01cc74173515cc3733f0af62b7d574364c1c68daf3ad748bca47e4328770cde", "variant": "freethreaded" }, "cpython-3.14.0b3+freethreaded-linux-armv7-gnueabi": { @@ -363,8 +363,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-armv7-unknown-linux-gnueabi-freethreaded%2Blto-full.tar.zst", - "sha256": "57d60ebe0a930faf07c7d2a59eadf3d5e59cf663492d0cadb91b4a936ec77319", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.14.0b3%2B20250702-armv7-unknown-linux-gnueabi-freethreaded%2Blto-full.tar.zst", + "sha256": "199ff8d1366007d666840a7320b0a44e6bab0aa0ee1e13e9247d3ec610ed9d45", "variant": "freethreaded" }, "cpython-3.14.0b3+freethreaded-linux-armv7-gnueabihf": { @@ -379,8 +379,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-armv7-unknown-linux-gnueabihf-freethreaded%2Blto-full.tar.zst", - "sha256": "966e2ec436d34cfb5c485c8a45029de8c7ab2501e97717a361c10bdf464e2287", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.14.0b3%2B20250702-armv7-unknown-linux-gnueabihf-freethreaded%2Blto-full.tar.zst", + "sha256": "e62adb4c3c7549bb909556457ac7863b98073bdcf5e6d9ffec52182b0fe32ccd", "variant": "freethreaded" }, "cpython-3.14.0b3+freethreaded-linux-powerpc64le-gnu": { @@ -395,8 +395,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-ppc64le-unknown-linux-gnu-freethreaded%2Blto-full.tar.zst", - "sha256": "9a036e9fff85e35dc7d9488604675bd139434e34020738b6e61a904e47567a50", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.14.0b3%2B20250702-ppc64le-unknown-linux-gnu-freethreaded%2Blto-full.tar.zst", + "sha256": "1f093e0c3532e27744e3fb73a8c738355910b6bfa195039e4f73b4f48c1bc4fc", "variant": "freethreaded" }, "cpython-3.14.0b3+freethreaded-linux-riscv64-gnu": { @@ -411,8 +411,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-riscv64-unknown-linux-gnu-freethreaded%2Blto-full.tar.zst", - "sha256": "875b77a12acf56f5b4349995271f387f0dd82e11c97b70d268070855b39b4bc4", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.14.0b3%2B20250702-riscv64-unknown-linux-gnu-freethreaded%2Blto-full.tar.zst", + "sha256": "73162a5da31cc1e410d456496114f8e5ee7243bc7bbe0e087b1ea50f0fdc6774", "variant": "freethreaded" }, "cpython-3.14.0b3+freethreaded-linux-s390x-gnu": { @@ -427,8 +427,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-s390x-unknown-linux-gnu-freethreaded%2Blto-full.tar.zst", - "sha256": "328fb2c96de92f38c69f5031ed0dd72e4d317593ac13dde0c1daadb2e554317b", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.14.0b3%2B20250702-s390x-unknown-linux-gnu-freethreaded%2Blto-full.tar.zst", + "sha256": "045017e60f1298111e8ccfec6afbe47abe56f82997258c8754009269a5343736", "variant": "freethreaded" }, "cpython-3.14.0b3+freethreaded-linux-x86_64-gnu": { @@ -443,8 +443,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-x86_64-unknown-linux-gnu-freethreaded%2Bpgo%2Blto-full.tar.zst", - "sha256": "fff6d39b2b03ea19fe191b1c64e67732b3f2d8f374bc6c6fd9bd94954d6287f2", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.14.0b3%2B20250702-x86_64-unknown-linux-gnu-freethreaded%2Bpgo%2Blto-full.tar.zst", + "sha256": "081f0147d8f4479764d6a3819f67275be3306003366eda9ecb9ee844f2f611be", "variant": "freethreaded" }, "cpython-3.14.0b3+freethreaded-linux-x86_64-musl": { @@ -459,8 +459,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-x86_64-unknown-linux-musl-freethreaded%2Blto-full.tar.zst", - "sha256": "b227cf474974ea4a92bc2a633e288456403f564251bf07b1a1ae288e0fac5551", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.14.0b3%2B20250702-x86_64-unknown-linux-musl-freethreaded%2Blto-full.tar.zst", + "sha256": "3e20f3c4757ca3d3738e2b4ed9bb7ce1b6b868b0f92e1766549b58bdfdf6ad79", "variant": "freethreaded" }, "cpython-3.14.0b3+freethreaded-linux-x86_64_v2-gnu": { @@ -475,8 +475,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-x86_64_v2-unknown-linux-gnu-freethreaded%2Bpgo%2Blto-full.tar.zst", - "sha256": "b9b4a72b211d97cafb37b66cb4a918b72af24bb4d5b957a96e978cad22a5cd67", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.14.0b3%2B20250702-x86_64_v2-unknown-linux-gnu-freethreaded%2Bpgo%2Blto-full.tar.zst", + "sha256": "7b50ca3a919531e6d175308d53efa0ccd3d21438ac735a51c7fdcd74c5316f99", "variant": "freethreaded" }, "cpython-3.14.0b3+freethreaded-linux-x86_64_v2-musl": { @@ -491,8 +491,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-x86_64_v2-unknown-linux-musl-freethreaded%2Blto-full.tar.zst", - "sha256": "f7898473617e67d3d6fd428a8c340ae340eb043d35e1d1584ed0497b3b933cb6", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.14.0b3%2B20250702-x86_64_v2-unknown-linux-musl-freethreaded%2Blto-full.tar.zst", + "sha256": "6787ae8dfa33268ae3336d9f2ff7107bb9da5714757cab2aed20bf916835888f", "variant": "freethreaded" }, "cpython-3.14.0b3+freethreaded-linux-x86_64_v3-gnu": { @@ -507,8 +507,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-x86_64_v3-unknown-linux-gnu-freethreaded%2Bpgo%2Blto-full.tar.zst", - "sha256": "7844c24cec06402fd6e437c62c347ea37ff685e2e1fda0fbc3076a70b2963296", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.14.0b3%2B20250702-x86_64_v3-unknown-linux-gnu-freethreaded%2Bpgo%2Blto-full.tar.zst", + "sha256": "6f16bffec9ad3717498b379b5640956abeb39b830ae390bb650585beac14b974", "variant": "freethreaded" }, "cpython-3.14.0b3+freethreaded-linux-x86_64_v3-musl": { @@ -523,8 +523,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-x86_64_v3-unknown-linux-musl-freethreaded%2Blto-full.tar.zst", - "sha256": "f39381f98e18a06fdc35137471f267d6de018bf0f6209e993a769c494b3e7fcd", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.14.0b3%2B20250702-x86_64_v3-unknown-linux-musl-freethreaded%2Blto-full.tar.zst", + "sha256": "651aef6d3640db60dbb0c28c68d194846053b3d08085427e1c9c76eb13de5e46", "variant": "freethreaded" }, "cpython-3.14.0b3+freethreaded-linux-x86_64_v4-gnu": { @@ -539,8 +539,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-x86_64_v4-unknown-linux-gnu-freethreaded%2Bpgo%2Blto-full.tar.zst", - "sha256": "c19941d7c0d7002ebad3d13947ad0231215ed1d6a9e93c8318e71dbaf6a9bbdd", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.14.0b3%2B20250702-x86_64_v4-unknown-linux-gnu-freethreaded%2Bpgo%2Blto-full.tar.zst", + "sha256": "637097da9317bd1af34a2f3baab76d98fb11aee3fb887dec4e829616d944cdb8", "variant": "freethreaded" }, "cpython-3.14.0b3+freethreaded-linux-x86_64_v4-musl": { @@ -555,8 +555,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-x86_64_v4-unknown-linux-musl-freethreaded%2Blto-full.tar.zst", - "sha256": "e7d6c051de697ed6f95a0560fde3eb79c20b2ea9ca94ef02c4f7ec406f813ec4", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.14.0b3%2B20250702-x86_64_v4-unknown-linux-musl-freethreaded%2Blto-full.tar.zst", + "sha256": "f607cd590190311cbe5f85d82d4220eb5b71416486b827e99b93ca1c341f2045", "variant": "freethreaded" }, "cpython-3.14.0b3+freethreaded-windows-aarch64-none": { @@ -571,8 +571,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-aarch64-pc-windows-msvc-freethreaded%2Bpgo-full.tar.zst", - "sha256": "986456a0e936f332a78a4130ea1114684d10b42b92b92d93b63da974767ae426", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.14.0b3%2B20250702-aarch64-pc-windows-msvc-freethreaded%2Bpgo-full.tar.zst", + "sha256": "331816d79cd78eaadba5ae6cdd3a243771199d0ca07057e7a452158dd4a7edcc", "variant": "freethreaded" }, "cpython-3.14.0b3+freethreaded-windows-i686-none": { @@ -587,8 +587,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-i686-pc-windows-msvc-freethreaded%2Bpgo-full.tar.zst", - "sha256": "dc630b0715cd1c7b23291715ce4fa62506ccd0578c4b186c5fd0a92a34cc6f0e", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.14.0b3%2B20250702-i686-pc-windows-msvc-freethreaded%2Bpgo-full.tar.zst", + "sha256": "2e55b7204f391fbe653168e6004daf5ed624d890ab7dd7d5aa7f7234e271ae47", "variant": "freethreaded" }, "cpython-3.14.0b3+freethreaded-windows-x86_64-none": { @@ -603,8 +603,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-x86_64-pc-windows-msvc-freethreaded%2Bpgo-full.tar.zst", - "sha256": "d136ddc9fcf1f27e5791dd3dd5edf755830f1eef65066af749760f350cea41c8", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.14.0b3%2B20250702-x86_64-pc-windows-msvc-freethreaded%2Bpgo-full.tar.zst", + "sha256": "8de6235b29396e3b25fc3ade166c49506171ec464cda46987ef9641dd9a44071", "variant": "freethreaded" }, "cpython-3.14.0b3+debug-linux-aarch64-gnu": { @@ -619,8 +619,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-aarch64-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "9bef0ad9e9c8c477c5a632074cb34d8ab3c25acaff95d7d7052dbabb952459c7", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.14.0b3%2B20250702-aarch64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "9bc39c6c669aaba047690395bf0955062aa80edb4fd92c59ada03a18a3df1234", "variant": "debug" }, "cpython-3.14.0b3+debug-linux-armv7-gnueabi": { @@ -635,8 +635,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-armv7-unknown-linux-gnueabi-debug-full.tar.zst", - "sha256": "28324fddb6699ce11ae62dc6458e963c44e82f045550ced84ad383b12a810472", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.14.0b3%2B20250702-armv7-unknown-linux-gnueabi-debug-full.tar.zst", + "sha256": "544dc6759e2d7af367eeb5d3c45116c52c33054a730e120a8ea442e6e8b9d091", "variant": "debug" }, "cpython-3.14.0b3+debug-linux-armv7-gnueabihf": { @@ -651,8 +651,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-armv7-unknown-linux-gnueabihf-debug-full.tar.zst", - "sha256": "74e896ddc8ec4b34f2f5c274132a96bb191b1524bc1627929084bec64ce2db62", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.14.0b3%2B20250702-armv7-unknown-linux-gnueabihf-debug-full.tar.zst", + "sha256": "3e91cd08cefd404d55003ec25347ae9d591e72ee77a00e2a172ce206c34f5ecc", "variant": "debug" }, "cpython-3.14.0b3+debug-linux-powerpc64le-gnu": { @@ -667,8 +667,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-ppc64le-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "127450fe5cb01a313870aa32d97740b0a5b3eaac2119a1590cabf66409fb31a4", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.14.0b3%2B20250702-ppc64le-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "b5679a4176431600ce146a6783046bbac84721d99ff91ead0b8eef1538514369", "variant": "debug" }, "cpython-3.14.0b3+debug-linux-riscv64-gnu": { @@ -683,8 +683,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-riscv64-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "7289b088a22731a0530c25395dd0617fe3d21fddc0c806206daa35b33d16f4ac", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.14.0b3%2B20250702-riscv64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "9724b0ebf2a8f80c1dd76bcb9880297bb2a95010bc707868145d9a1cfa0857de", "variant": "debug" }, "cpython-3.14.0b3+debug-linux-s390x-gnu": { @@ -699,8 +699,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-s390x-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "356b05d8d5c612bbd29acaed04dc3c54d8ea42af0d31608370b04cce5093c4e7", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.14.0b3%2B20250702-s390x-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "23ca40a78ad8a61fc820d58b71e5aeb3b5f88ed7e449a04c0515b37041e8e644", "variant": "debug" }, "cpython-3.14.0b3+debug-linux-x86_64-gnu": { @@ -715,8 +715,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-x86_64-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "3092e467a21a4f00d030f0ac3efb97e49939debe3c00ed8c80cfd25c0db238f7", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.14.0b3%2B20250702-x86_64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "84129181fc24fd5fd39a8dc83c5eb4dd7c51a9f105bd1b22733dba2d52da9f38", "variant": "debug" }, "cpython-3.14.0b3+debug-linux-x86_64-musl": { @@ -731,8 +731,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-x86_64-unknown-linux-musl-debug-full.tar.zst", - "sha256": "d8391e345823d2eaf0745ddb77982a35c032674e1d08ec29321568c21737a15e", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.14.0b3%2B20250702-x86_64-unknown-linux-musl-debug-full.tar.zst", + "sha256": "bfaaabee0e9cab4a7967be9759140830de1994c8f87e8e05bee5ec7fd6a99b69", "variant": "debug" }, "cpython-3.14.0b3+debug-linux-x86_64_v2-gnu": { @@ -747,8 +747,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "3f497ae10d574532c0a79b4704ba7944ee41586e86997ec1933c99efdfe19ec3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.14.0b3%2B20250702-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "c14586447c4ef79ab875b7b7b8a13e6d05eaec8627f187067e02f4b026023db6", "variant": "debug" }, "cpython-3.14.0b3+debug-linux-x86_64_v2-musl": { @@ -763,8 +763,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-x86_64_v2-unknown-linux-musl-debug-full.tar.zst", - "sha256": "da154e3008bc4bd4185993bc04b355b79fbcd63a900b049d4595ea3615aef76c", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.14.0b3%2B20250702-x86_64_v2-unknown-linux-musl-debug-full.tar.zst", + "sha256": "69c477df92e4332382e9a1b3177155b1c2c9e6612366b385409bd17f18c49a70", "variant": "debug" }, "cpython-3.14.0b3+debug-linux-x86_64_v3-gnu": { @@ -779,8 +779,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "a64d5b16c684700b58cff51164c0faca6dc4de3dc6509ea958f975217fb1f804", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.14.0b3%2B20250702-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "5a9c969834b90307152a8bdcef27a2797288fdfecb92911e0ebc17ec5747ccbf", "variant": "debug" }, "cpython-3.14.0b3+debug-linux-x86_64_v3-musl": { @@ -795,8 +795,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-x86_64_v3-unknown-linux-musl-debug-full.tar.zst", - "sha256": "ced306fbaf48996b422c5e45125de61578fe1a7b3f458bd4173f462b6b691fd2", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.14.0b3%2B20250702-x86_64_v3-unknown-linux-musl-debug-full.tar.zst", + "sha256": "02fad0b21f30b42742468107fe33eb23d307ba2c5670b0baa11e33fc30160fba", "variant": "debug" }, "cpython-3.14.0b3+debug-linux-x86_64_v4-gnu": { @@ -811,8 +811,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "68a7b564d38fafd2c0b864adcdc3595830d58714093951a5412ad72c021b302a", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.14.0b3%2B20250702-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "4e110ee96813a907c7468f5c1317046c5e5ba10e2fe23b2c3d30f1ee6b4bc5c7", "variant": "debug" }, "cpython-3.14.0b3+debug-linux-x86_64_v4-musl": { @@ -827,8 +827,8 @@ "minor": 14, "patch": 0, "prerelease": "b3", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.14.0b3%2B20250630-x86_64_v4-unknown-linux-musl-debug-full.tar.zst", - "sha256": "7213fcbc7628c5a15cf3fb1c8bff43d0b8a5473fd86cf46941f7ee00f2b9d136", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.14.0b3%2B20250702-x86_64_v4-unknown-linux-musl-debug-full.tar.zst", + "sha256": "6015df86031d23765c7f4c8a02e1aa3e3b5e4a5fe9f2747fcdc37d28e3f8a0f5", "variant": "debug" }, "cpython-3.14.0b2-darwin-aarch64-none": { @@ -5563,8 +5563,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-aarch64-apple-darwin-install_only_stripped.tar.gz", - "sha256": "a36fb4e711c2c7987d541aa5682db80fc7e7b72205bae6c330bf992d23db576f", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.13.5%2B20250702-aarch64-apple-darwin-install_only_stripped.tar.gz", + "sha256": "5a7888b6e0bbc2abf7327a786d50f46f36b941f43268ce05d6ef6f1f733734ca", "variant": null }, "cpython-3.13.5-darwin-x86_64-none": { @@ -5579,8 +5579,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-x86_64-apple-darwin-install_only_stripped.tar.gz", - "sha256": "81b51eec47af8cd63d7fbcc809bd0ae3e07966a549620b159598525788961fdc", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.13.5%2B20250702-x86_64-apple-darwin-install_only_stripped.tar.gz", + "sha256": "691282c117b01de296d70bd3f2ec2d7316620233626440b35fa2271ddbcc07dc", "variant": null }, "cpython-3.13.5-linux-aarch64-gnu": { @@ -5595,8 +5595,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "906ffb000e53469921b0c3cfbcdab88e7fbf8a29cd48dec8cb9a9b8146947e1d", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.13.5%2B20250702-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "d7ab196fefb0cacb44869774dd6afcaed2edc90219b67601ec1875002378219f", "variant": null }, "cpython-3.13.5-linux-armv7-gnueabi": { @@ -5611,8 +5611,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-armv7-unknown-linux-gnueabi-install_only_stripped.tar.gz", - "sha256": "5cae766871606d74466e54fe8260ce81d74e1d545eb78da8d4986a910d8c418a", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.13.5%2B20250702-armv7-unknown-linux-gnueabi-install_only_stripped.tar.gz", + "sha256": "b90aac358d49e0c278843b83b5c8935036effe10f004ecec903313fea199badf", "variant": null }, "cpython-3.13.5-linux-armv7-gnueabihf": { @@ -5627,8 +5627,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-armv7-unknown-linux-gnueabihf-install_only_stripped.tar.gz", - "sha256": "50e3cd7c633991c1b1d48a00a708ff002437f96e4b8400f93f97e237b2777208", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.13.5%2B20250702-armv7-unknown-linux-gnueabihf-install_only_stripped.tar.gz", + "sha256": "69fe828cd149b21a6fda0937f73ef654dd8237d567916508acb328f24f9368c7", "variant": null }, "cpython-3.13.5-linux-powerpc64le-gnu": { @@ -5643,8 +5643,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-ppc64le-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "5f2b7dbecf27e21afa5c3a6e203be0bd36a325b422b12bc3526de475e2566c3f", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.13.5%2B20250702-ppc64le-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "1a69f799fc5c0eb61708202ec5ba1514d6e5f3a547c07c53d40415d93084b903", "variant": null }, "cpython-3.13.5-linux-riscv64-gnu": { @@ -5659,8 +5659,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-riscv64-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "713da45173f786f38e22f56d49b495647d93713dd9a0ec8bd8aa1f9e8e87ac1e", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.13.5%2B20250702-riscv64-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "2ce3570c6f3f8204e4b5e8e35896c87c71ddc038ca9a60f1111e2ea468b78f08", "variant": null }, "cpython-3.13.5-linux-s390x-gnu": { @@ -5675,8 +5675,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-s390x-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "c016223dcbfb5f3d686db67ba6d489426de21076d356640dcb0f8bf002573b43", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.13.5%2B20250702-s390x-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "c974786ad18943fc3d5fbe4eca7bd43ceb07e725d2d513ac4dc0e3b6dd11a89e", "variant": null }, "cpython-3.13.5-linux-x86_64-gnu": { @@ -5691,8 +5691,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "3057ed944be60b0d636aebfdde12dedb62c6554f33b5b50b8806dbc15a502324", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.13.5%2B20250702-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "4444b5e95217c2c686bf3a689ab9655d47ea3b11c1f74714eceab021d50b7d74", "variant": null }, "cpython-3.13.5-linux-x86_64-musl": { @@ -5707,8 +5707,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-x86_64-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "e26d9d405c2f224ac6d989b82d99fd1e3468d2dfaf288ac5041f743fdea327c0", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.13.5%2B20250702-x86_64-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "5125ef4d58b3dddbb0946c29f443160de95d6e8ea79bbe9562f9dd2873651d12", "variant": null }, "cpython-3.13.5-linux-x86_64_v2-gnu": { @@ -5723,8 +5723,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-x86_64_v2-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "3041155088ef88fd28981f268de5b47d243ac9a2cffa3582fc4c1a6c633bb653", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.13.5%2B20250702-x86_64_v2-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "91cf219324d090e7b2814f33c2b7fbf4930aa01c6a0fd8960eab8874f3b8babd", "variant": null }, "cpython-3.13.5-linux-x86_64_v2-musl": { @@ -5739,8 +5739,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-x86_64_v2-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "1ec20ff2606e5510b30e0cb56ebe06b806b08c13712007d7550d26a35bddc9bd", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.13.5%2B20250702-x86_64_v2-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "392dd5fd090f9aa5759318795733e37026cf13d554bcf5f90315d0f448a07228", "variant": null }, "cpython-3.13.5-linux-x86_64_v3-gnu": { @@ -5755,8 +5755,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-x86_64_v3-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "e62614725c2b31060063a5a54f24d01b90cd0f2418c6ff14c61cfad0f81cfefd", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.13.5%2B20250702-x86_64_v3-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "c1bd09a8f83a78dd92a6d0e2a8dbf30b99d6ca31269fd1c80e14f0769b67df3f", "variant": null }, "cpython-3.13.5-linux-x86_64_v3-musl": { @@ -5771,8 +5771,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-x86_64_v3-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "9a02ef405e8d811c4cf703aa237c428f9460b4857465f6b4a5c23fce4948e887", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.13.5%2B20250702-x86_64_v3-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "77350988d48699f3172a25aad33b229604b6faab9f1df35589ad7aca10ec10a8", "variant": null }, "cpython-3.13.5-linux-x86_64_v4-gnu": { @@ -5787,8 +5787,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-x86_64_v4-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "f06da893a220d6e97f845292c7623743b79167047c0d97ead6b2baa927d9d26b", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.13.5%2B20250702-x86_64_v4-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "f84aafa52b22484de42edb7c9965cafc52718fc03ac5f8d5ad6a92eb46ff3008", "variant": null }, "cpython-3.13.5-linux-x86_64_v4-musl": { @@ -5803,8 +5803,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-x86_64_v4-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "a16a169ac9675e82ab190528e93cfbc7ab86f446485748afe1138010753eebd4", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.13.5%2B20250702-x86_64_v4-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "9510ffc50a28a1a06d5c1ed2bfd18fa0f469d5e93982d7a9289ab0ac4c8a2eee", "variant": null }, "cpython-3.13.5-windows-aarch64-none": { @@ -5819,8 +5819,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-aarch64-pc-windows-msvc-install_only_stripped.tar.gz", - "sha256": "bc82935e2102d265b298fb1618930b36930fc1820d8dc2f6e3c2e34767917f44", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.13.5%2B20250702-aarch64-pc-windows-msvc-install_only_stripped.tar.gz", + "sha256": "2459713eff80372e0bfcce42b23b9892eb7e3b21ea6ae5cb5e504b8c0f12e6dd", "variant": null }, "cpython-3.13.5-windows-i686-none": { @@ -5835,8 +5835,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-i686-pc-windows-msvc-install_only_stripped.tar.gz", - "sha256": "7ffe48c8242d24b029ae880a0651076ec3eb9b2a23a71f9ad170bee37cc40f42", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.13.5%2B20250702-i686-pc-windows-msvc-install_only_stripped.tar.gz", + "sha256": "76287476f3a2b8658d29da67e05d550fbf2db33b9e9730c6d071bd239211ffe8", "variant": null }, "cpython-3.13.5-windows-x86_64-none": { @@ -5851,8 +5851,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-x86_64-pc-windows-msvc-install_only_stripped.tar.gz", - "sha256": "5f7b96c86d4bea7a823d0fbce8e139a39acad3560bb0b8ffaae236c0bf8251f9", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.13.5%2B20250702-x86_64-pc-windows-msvc-install_only_stripped.tar.gz", + "sha256": "f5838b42985c644d0597a1a6a54fb185647bb57d4f06cbc7d3ac8dfb53326521", "variant": null }, "cpython-3.13.5+freethreaded-darwin-aarch64-none": { @@ -5867,8 +5867,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-aarch64-apple-darwin-freethreaded%2Bpgo%2Blto-full.tar.zst", - "sha256": "71772559da8e7155ae34ee9e625126bb76df42d66184b533f82c74b7acd0b9f0", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.13.5%2B20250702-aarch64-apple-darwin-freethreaded%2Bpgo%2Blto-full.tar.zst", + "sha256": "52e582cc89d654c565297b4ff9c3bd4bed5c3e81cad46f41c62485e700faf8bd", "variant": "freethreaded" }, "cpython-3.13.5+freethreaded-darwin-x86_64-none": { @@ -5883,8 +5883,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-x86_64-apple-darwin-freethreaded%2Bpgo%2Blto-full.tar.zst", - "sha256": "e31c161b78c141e9fde65e7a37ac0e944ac2f8fb0e001b49244deae824ed0343", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.13.5%2B20250702-x86_64-apple-darwin-freethreaded%2Bpgo%2Blto-full.tar.zst", + "sha256": "5aed6d5950514004149d514f81a1cd426ac549696a563b8e47d32f7eba3b4be3", "variant": "freethreaded" }, "cpython-3.13.5+freethreaded-linux-aarch64-gnu": { @@ -5899,8 +5899,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-aarch64-unknown-linux-gnu-freethreaded%2Blto-full.tar.zst", - "sha256": "578fcc7c2f8e9816ccd7302a5f06f3b295e1059249f594b70a15eb6b744405e9", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.13.5%2B20250702-aarch64-unknown-linux-gnu-freethreaded%2Bpgo%2Blto-full.tar.zst", + "sha256": "461832e4fb5ec1d719dc40f6490f9a639414dfa6769158187fa85d4b424b57cd", "variant": "freethreaded" }, "cpython-3.13.5+freethreaded-linux-armv7-gnueabi": { @@ -5915,8 +5915,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-armv7-unknown-linux-gnueabi-freethreaded%2Blto-full.tar.zst", - "sha256": "60be2a6390408fe4ff1def8b5841240749f6e036be0719aa4120dc6412657467", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.13.5%2B20250702-armv7-unknown-linux-gnueabi-freethreaded%2Blto-full.tar.zst", + "sha256": "469fc30158fbcb5c3dc7e65e0d7d9e9e0f4de7dffdc97492083781f5f6216356", "variant": "freethreaded" }, "cpython-3.13.5+freethreaded-linux-armv7-gnueabihf": { @@ -5931,8 +5931,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-armv7-unknown-linux-gnueabihf-freethreaded%2Blto-full.tar.zst", - "sha256": "e477210f0b1c66e3811433bb1d45d7336e9e0bb52f4ec2a4d6a789dbdd9de9c2", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.13.5%2B20250702-armv7-unknown-linux-gnueabihf-freethreaded%2Blto-full.tar.zst", + "sha256": "9db3d9dbb529068d24b46c0616307f3c278e59c0087d7a1637105afde3bc5685", "variant": "freethreaded" }, "cpython-3.13.5+freethreaded-linux-powerpc64le-gnu": { @@ -5947,8 +5947,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-ppc64le-unknown-linux-gnu-freethreaded%2Blto-full.tar.zst", - "sha256": "4d00d210cf083c83950e5af59df044c34538c15f823a6b07c667df2479bbcb21", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.13.5%2B20250702-ppc64le-unknown-linux-gnu-freethreaded%2Blto-full.tar.zst", + "sha256": "c65c75edb450de830f724afdc774a215c2d3255097e0d670f709d2271fd6fd52", "variant": "freethreaded" }, "cpython-3.13.5+freethreaded-linux-riscv64-gnu": { @@ -5963,8 +5963,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-riscv64-unknown-linux-gnu-freethreaded%2Blto-full.tar.zst", - "sha256": "63408df03d6d6ec22a9d4e01701a1519247ceb35804161d64f0aed0d73154087", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.13.5%2B20250702-riscv64-unknown-linux-gnu-freethreaded%2Blto-full.tar.zst", + "sha256": "716e6e3fad24fb9931b93005000152dd9da4c3343b88ca54b5c01a7ab879d734", "variant": "freethreaded" }, "cpython-3.13.5+freethreaded-linux-s390x-gnu": { @@ -5979,8 +5979,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-s390x-unknown-linux-gnu-freethreaded%2Blto-full.tar.zst", - "sha256": "af58974a23754b02301043d6e810fb3ce533d1c34c23d197a6690f3eb2f00fe1", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.13.5%2B20250702-s390x-unknown-linux-gnu-freethreaded%2Blto-full.tar.zst", + "sha256": "27276aee426a51f4165fac49391aedc5a9e301ae217366c77b65826122bb30fc", "variant": "freethreaded" }, "cpython-3.13.5+freethreaded-linux-x86_64-gnu": { @@ -5995,8 +5995,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-x86_64-unknown-linux-gnu-freethreaded%2Bpgo%2Blto-full.tar.zst", - "sha256": "976133de595393596207fa3c943d4e8e43ddb5d51b864766fc666a1752963e04", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.13.5%2B20250702-x86_64-unknown-linux-gnu-freethreaded%2Bpgo%2Blto-full.tar.zst", + "sha256": "f5eb29604c0b7afa2097fca094a06eb7a1f3ca4e194264c34f342739cae78202", "variant": "freethreaded" }, "cpython-3.13.5+freethreaded-linux-x86_64-musl": { @@ -6011,8 +6011,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-x86_64-unknown-linux-musl-freethreaded%2Blto-full.tar.zst", - "sha256": "f11c199765a4c96e93bd5ffc65477dc7eb4c4edd03f20ecf4d51ea0afc3be0c2", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.13.5%2B20250702-x86_64-unknown-linux-musl-freethreaded%2Blto-full.tar.zst", + "sha256": "61f5960849c547313ff7142990ec8a8c1e299ccf3fcba00600bc8ee50fbb0db9", "variant": "freethreaded" }, "cpython-3.13.5+freethreaded-linux-x86_64_v2-gnu": { @@ -6027,8 +6027,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-x86_64_v2-unknown-linux-gnu-freethreaded%2Bpgo%2Blto-full.tar.zst", - "sha256": "ff8066512111ac80962bdd094043f600eb9dbdd35e93f059f5ab34f9c4ff412e", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.13.5%2B20250702-x86_64_v2-unknown-linux-gnu-freethreaded%2Bpgo%2Blto-full.tar.zst", + "sha256": "6348a6ca86e8cfe30557fecfc15d6facefeeecb55aba33c338d6aa5830495e5b", "variant": "freethreaded" }, "cpython-3.13.5+freethreaded-linux-x86_64_v2-musl": { @@ -6043,8 +6043,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-x86_64_v2-unknown-linux-musl-freethreaded%2Blto-full.tar.zst", - "sha256": "26ab90c03672187ae4a330e59bf5142cbe7ed24e576edfd3b869fa108a3b3fc7", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.13.5%2B20250702-x86_64_v2-unknown-linux-musl-freethreaded%2Blto-full.tar.zst", + "sha256": "4200aa24f5ca3b1621185fe0aee642f84e91ec353e0da2ca47a62c369855d07a", "variant": "freethreaded" }, "cpython-3.13.5+freethreaded-linux-x86_64_v3-gnu": { @@ -6059,8 +6059,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-x86_64_v3-unknown-linux-gnu-freethreaded%2Bpgo%2Blto-full.tar.zst", - "sha256": "15e614a96bf2d789500c9bee03c7330e61278c1b9b4f3138f38bfa05bbe4bd68", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.13.5%2B20250702-x86_64_v3-unknown-linux-gnu-freethreaded%2Bpgo%2Blto-full.tar.zst", + "sha256": "17ada48150f49c1d9aefc10839708d6f31f2665fa625103d45ccf88af46c9674", "variant": "freethreaded" }, "cpython-3.13.5+freethreaded-linux-x86_64_v3-musl": { @@ -6075,8 +6075,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-x86_64_v3-unknown-linux-musl-freethreaded%2Blto-full.tar.zst", - "sha256": "448f46715607f34cc217ce2a51d0782417d99077b5ecd8330d2c53b74e893a0c", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.13.5%2B20250702-x86_64_v3-unknown-linux-musl-freethreaded%2Blto-full.tar.zst", + "sha256": "b8a951e4eb04042af91864f3934e8e8b7527e390720ba68507a4b9fe4143032b", "variant": "freethreaded" }, "cpython-3.13.5+freethreaded-linux-x86_64_v4-gnu": { @@ -6091,8 +6091,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-x86_64_v4-unknown-linux-gnu-freethreaded%2Bpgo%2Blto-full.tar.zst", - "sha256": "6620f3bbd38732ce232d84abbad1836400a680a1208916af502bbeebc8ae825d", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.13.5%2B20250702-x86_64_v4-unknown-linux-gnu-freethreaded%2Bpgo%2Blto-full.tar.zst", + "sha256": "7bb14c995b2bc7b0a330a8e7b33d103d9f99ecb9c30ff8ca621d9d066bb63d9f", "variant": "freethreaded" }, "cpython-3.13.5+freethreaded-linux-x86_64_v4-musl": { @@ -6107,8 +6107,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-x86_64_v4-unknown-linux-musl-freethreaded%2Blto-full.tar.zst", - "sha256": "895e650967c266182f64c8f3a3fd395d336bbd95218fd0c7affc82f3c9465899", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.13.5%2B20250702-x86_64_v4-unknown-linux-musl-freethreaded%2Blto-full.tar.zst", + "sha256": "a78845959c6f816f9a0fa602403b339d67d7125515f5b0fbe5c0ef393e4ce4e9", "variant": "freethreaded" }, "cpython-3.13.5+freethreaded-windows-aarch64-none": { @@ -6123,8 +6123,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-aarch64-pc-windows-msvc-freethreaded%2Bpgo-full.tar.zst", - "sha256": "4dc24bdf390356281dd23a4644565e7f49187894015b8292c0257a0f5cd2fd43", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.13.5%2B20250702-aarch64-pc-windows-msvc-freethreaded%2Bpgo-full.tar.zst", + "sha256": "97041594d903d6a1de1e55e9a3e5c613384aa7b900a93096f372732d9953f52a", "variant": "freethreaded" }, "cpython-3.13.5+freethreaded-windows-i686-none": { @@ -6139,8 +6139,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-i686-pc-windows-msvc-freethreaded%2Bpgo-full.tar.zst", - "sha256": "009f56db3de1351dd616fb1fd8b04343637dd84b01c1eb37af8559fa87c6b423", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.13.5%2B20250702-i686-pc-windows-msvc-freethreaded%2Bpgo-full.tar.zst", + "sha256": "02d20b1521420ef84160c3a781928cdc49cd2e39b3850fb26f01e4e643b8379e", "variant": "freethreaded" }, "cpython-3.13.5+freethreaded-windows-x86_64-none": { @@ -6155,8 +6155,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-x86_64-pc-windows-msvc-freethreaded%2Bpgo-full.tar.zst", - "sha256": "7387f13148676858b9ab61ff7f03b82b88d81d0342346d1a156233b9478f3c81", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.13.5%2B20250702-x86_64-pc-windows-msvc-freethreaded%2Bpgo-full.tar.zst", + "sha256": "39e19dcb823a2ed47d9510753a642ba468802f1c5e15771c6c22814f4acada94", "variant": "freethreaded" }, "cpython-3.13.5+debug-linux-aarch64-gnu": { @@ -6171,8 +6171,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-aarch64-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "0698a989ead503f3181118a8c2793b431707190a1ef82fe2a5386a268ebe58d6", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.13.5%2B20250702-aarch64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "fc7e5a8765892887b887b31eaa03b952405c98ad0b79bf412489810ab4872a18", "variant": "debug" }, "cpython-3.13.5+debug-linux-armv7-gnueabi": { @@ -6187,8 +6187,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-armv7-unknown-linux-gnueabi-debug-full.tar.zst", - "sha256": "0a5d4cec664ede542ad92115f2c90318bed8020d093ec98a9cf935374d0b684e", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.13.5%2B20250702-armv7-unknown-linux-gnueabi-debug-full.tar.zst", + "sha256": "61e5a7e1c6bd86db10302899fe1053f54815a4d3e846ad3e8d4ebc5a858aa1ae", "variant": "debug" }, "cpython-3.13.5+debug-linux-armv7-gnueabihf": { @@ -6203,8 +6203,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-armv7-unknown-linux-gnueabihf-debug-full.tar.zst", - "sha256": "6be78a4c8df0a4ee420e2a51b6f5581d7edf960b38bcf38c1a4f3ecee018b248", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.13.5%2B20250702-armv7-unknown-linux-gnueabihf-debug-full.tar.zst", + "sha256": "e400e3e2ae88108034e62076edbc5518974eb76a46d236b5322fa7b2aa2110f4", "variant": "debug" }, "cpython-3.13.5+debug-linux-powerpc64le-gnu": { @@ -6219,8 +6219,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-ppc64le-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "bba8b2e8c0a6095460c519c273d7e101c616c96e2992407301018d00a5d7b686", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.13.5%2B20250702-ppc64le-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "1216b39a34397f25065f80bb6f3ffa56f364da9dae09a519df9d384c3a1c7505", "variant": "debug" }, "cpython-3.13.5+debug-linux-riscv64-gnu": { @@ -6235,8 +6235,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-riscv64-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "10be77859e5f99a83c07e4a521776d2a8860dd46ce8a46c1e3c2544d2593702a", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.13.5%2B20250702-riscv64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "f2212f189076e6631e4447cc0c37872d03fc39eb92bb717a922899852e17765b", "variant": "debug" }, "cpython-3.13.5+debug-linux-s390x-gnu": { @@ -6251,8 +6251,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-s390x-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "05e668c47ba1c75f0df0c01b6afb3af39ae9d17a90d02f7f6508c094de79aa3c", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.13.5%2B20250702-s390x-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "f672e057e98d5909b6ef94558018036254d4d4e40660cfb1654ce2c3b87bcd82", "variant": "debug" }, "cpython-3.13.5+debug-linux-x86_64-gnu": { @@ -6267,8 +6267,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-x86_64-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "5d8f83f26f8f203eb888ae42e02d6f85c26ac0306d65ad311e79ca2120095ce1", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.13.5%2B20250702-x86_64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "3e798c809c4a9fc7308626ff72540036d5f01f9ac85ce6176acbdd581e973302", "variant": "debug" }, "cpython-3.13.5+debug-linux-x86_64-musl": { @@ -6283,8 +6283,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-x86_64-unknown-linux-musl-debug-full.tar.zst", - "sha256": "70705fd017908822cbfad83b444f4fc46301467f13e17c97de8e456ee9f08173", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.13.5%2B20250702-x86_64-unknown-linux-musl-debug-full.tar.zst", + "sha256": "1d5a3c193673b52eab5c5a362a18e6e184e4298a36a809fe5e21be6f01f9b76f", "variant": "debug" }, "cpython-3.13.5+debug-linux-x86_64_v2-gnu": { @@ -6299,8 +6299,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "064e7eb23cce3b5ceb22d974d14c80a0ffbe309178d60d56ee910deda69c3bb2", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.13.5%2B20250702-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "8886ff6fd5483254a234e4ce52b4707147bc493f6556fa9869da4d1264af9440", "variant": "debug" }, "cpython-3.13.5+debug-linux-x86_64_v2-musl": { @@ -6315,8 +6315,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-x86_64_v2-unknown-linux-musl-debug-full.tar.zst", - "sha256": "f875a778f5c1f4a3f8c0c37ebdd2437eea9abcfa3eff4d161ee8d761d3018892", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.13.5%2B20250702-x86_64_v2-unknown-linux-musl-debug-full.tar.zst", + "sha256": "8894486f97353fd0279fd7e4d107625aa57c68010c6fc8fcba6a549e3c4aa499", "variant": "debug" }, "cpython-3.13.5+debug-linux-x86_64_v3-gnu": { @@ -6331,8 +6331,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "7024d12c2241595cb3c66a0c291d416cc431198aa01d8c2d642fad3266b4d2c8", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.13.5%2B20250702-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "914baf954e3decbe182413b38a8a5c347246e889a4d34a91db3f4466945dba0a", "variant": "debug" }, "cpython-3.13.5+debug-linux-x86_64_v3-musl": { @@ -6347,8 +6347,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-x86_64_v3-unknown-linux-musl-debug-full.tar.zst", - "sha256": "36ce0d9ca8163798c83d027d0abee2ed78ae0a91df96346ed5b7c26700131f38", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.13.5%2B20250702-x86_64_v3-unknown-linux-musl-debug-full.tar.zst", + "sha256": "9c5de2ef49b4b871157e81cd7a0f4e881971d0c16873e6ad6376ace2914c07c5", "variant": "debug" }, "cpython-3.13.5+debug-linux-x86_64_v4-gnu": { @@ -6363,8 +6363,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "31c2c46f031fd32b2a8955a9193365f8e56f06e4b598b72d34441b4461ca55e7", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.13.5%2B20250702-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "48f0eaeeac55dbe85593e659962e6ea44cc638f74cc0aab1d943da805a4eca39", "variant": "debug" }, "cpython-3.13.5+debug-linux-x86_64_v4-musl": { @@ -6379,8 +6379,8 @@ "minor": 13, "patch": 5, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.13.5%2B20250630-x86_64_v4-unknown-linux-musl-debug-full.tar.zst", - "sha256": "1129ba59de94be77f3ed9d4fccb5f48f9f297600b71aeea79d20f170f84b4323", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.13.5%2B20250702-x86_64_v4-unknown-linux-musl-debug-full.tar.zst", + "sha256": "d0b7928f0e56c3509d541ecb5538d93d0dd673ba6460159f0d05c6a453c575c4", "variant": "debug" }, "cpython-3.13.4-darwin-aarch64-none": { @@ -10603,8 +10603,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-aarch64-apple-darwin-install_only_stripped.tar.gz", - "sha256": "52f103b1bae4105c8c3ba415fbfb992fc80672795c58262b96719efde97c49d9", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.12.11%2B20250702-aarch64-apple-darwin-install_only_stripped.tar.gz", + "sha256": "0f3a56aeca07b511050210932e035a3b325abb032fca1e6b5d571f19cc93bc5b", "variant": null }, "cpython-3.12.11-darwin-x86_64-none": { @@ -10619,8 +10619,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-x86_64-apple-darwin-install_only_stripped.tar.gz", - "sha256": "5e7229d72ec588b77f753ee043fcefe62f89190e84b8b43d20b1be4b4e6e8540", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.12.11%2B20250702-x86_64-apple-darwin-install_only_stripped.tar.gz", + "sha256": "1543bcace1e0aadc5cdcc4a750202a7faa9be21fb50782aee67824f26f2668ad", "variant": null }, "cpython-3.12.11-linux-aarch64-gnu": { @@ -10635,8 +10635,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "117b1d2daa96486005827dd50029fecd61e99141396583ea8e57733f7cf0abad", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.12.11%2B20250702-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "1b7260c6aa4d3c7d27c5fc5750e6ece2312cf24e56c60239d55b5ad7a96b17cb", "variant": null }, "cpython-3.12.11-linux-armv7-gnueabi": { @@ -10651,8 +10651,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-armv7-unknown-linux-gnueabi-install_only_stripped.tar.gz", - "sha256": "878d29bab42a2911927948636a1bdd1e83ac1d31dbe4b857a214b04eed2046de", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.12.11%2B20250702-armv7-unknown-linux-gnueabi-install_only_stripped.tar.gz", + "sha256": "b977bb031eeffcf07b7373c509601dd26963df1a42964196fccf193129be6e3b", "variant": null }, "cpython-3.12.11-linux-armv7-gnueabihf": { @@ -10667,8 +10667,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-armv7-unknown-linux-gnueabihf-install_only_stripped.tar.gz", - "sha256": "8a3ae17b0ecaa0334ca1bbd3d2cc7ea16d43ead269c03cdaedee54bfd6949962", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.12.11%2B20250702-armv7-unknown-linux-gnueabihf-install_only_stripped.tar.gz", + "sha256": "bf67827338f2b17443e6df04b19827ed2e8e4072850b18d4feca70ba26ba2d56", "variant": null }, "cpython-3.12.11-linux-powerpc64le-gnu": { @@ -10683,8 +10683,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-ppc64le-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "bb0689d5101a4a85eaca7e8443912d196519103695937fb8e4fc819d66e48167", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.12.11%2B20250702-ppc64le-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "22f894d8e6d6848c4bc9ead18961febeaaecfea65bcf97ccc3ca1bd4fdcd4f70", "variant": null }, "cpython-3.12.11-linux-riscv64-gnu": { @@ -10699,8 +10699,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-riscv64-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "59323d58c55f3afc90d93d185f57bdfc9bbfcf1e713290df6eda91fbabf91a90", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.12.11%2B20250702-riscv64-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "96c5c30c57d5fd1bdb705bfe73170462201a519f8a27cc0a394cd4ed875ae535", "variant": null }, "cpython-3.12.11-linux-s390x-gnu": { @@ -10715,8 +10715,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-s390x-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "925c52b00a55b52492c197ebc7499317e4403cf603f1670a6e655b9426d286f2", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.12.11%2B20250702-s390x-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "1967e03dd3d1f8137d29556eec8f7a51890816fd71c8b37060bd061bce74715a", "variant": null }, "cpython-3.12.11-linux-x86_64-gnu": { @@ -10731,8 +10731,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "358ad7bf46260a2e1b31726adc62dc04258c7ea7469352e155ffdea0f168499e", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.12.11%2B20250702-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "2eed351d3f6e99b4b6d1fe1f4202407fe041d799585dffdf6d93c49d1f899e37", "variant": null }, "cpython-3.12.11-linux-x86_64-musl": { @@ -10747,8 +10747,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-x86_64-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "63bcf41a095c298d412efdb9ceba0009e8544809fe0fa9b1ba51dc918f3dc3ee", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.12.11%2B20250702-x86_64-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "8b201f157e437f4f3777e469b50f8e23dfa02f1c6757dfb2a19bde9f1bae9e0a", "variant": null }, "cpython-3.12.11-linux-x86_64_v2-gnu": { @@ -10763,8 +10763,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-x86_64_v2-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "49de0e676faa6e72a988ebe41d67cd433d074ab4fd3b555935d0d4f7437f7168", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.12.11%2B20250702-x86_64_v2-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "978249b5f885216be70d6723f51f5d6ad17628bacc2b1b98078e1273326ef847", "variant": null }, "cpython-3.12.11-linux-x86_64_v2-musl": { @@ -10779,8 +10779,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-x86_64_v2-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "66f322f7eb520a441ef341b166e9cbd45df83e5841fd4998181f431f719f5a8c", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.12.11%2B20250702-x86_64_v2-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "b81b771718ed550c4476df3c07d38383a2c9341e2e912fd58c224820cb18195c", "variant": null }, "cpython-3.12.11-linux-x86_64_v3-gnu": { @@ -10795,8 +10795,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-x86_64_v3-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "be9ba63f7905b925d958aebcb8fcec40a2ba94552e2bd3d91818d97fc3d68ecb", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.12.11%2B20250702-x86_64_v3-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "651f0da21ac842276f9c3f955a3f3f87d0ad6ec1bba7c3bb8079c3f4752355b3", "variant": null }, "cpython-3.12.11-linux-x86_64_v3-musl": { @@ -10811,8 +10811,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-x86_64_v3-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "598661a23fd5c0f2870c99194938732a3e1a4909e41b8a54401b821f1594e66b", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.12.11%2B20250702-x86_64_v3-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "4d7cbbf546b75623d0f7510befd2cf0a942b8bc0a38d82876f0844383aa27ba2", "variant": null }, "cpython-3.12.11-linux-x86_64_v4-gnu": { @@ -10827,8 +10827,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-x86_64_v4-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "b76430b697fa0c1ef1b2b04361decf36b7108da719c48ca2098e6aa0cd9a7130", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.12.11%2B20250702-x86_64_v4-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "b368b2dd0939f9e847acb5b0851081dcf2151e553bea4ac6f266a6ca0daeca01", "variant": null }, "cpython-3.12.11-linux-x86_64_v4-musl": { @@ -10843,8 +10843,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-x86_64_v4-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "88121676a946fe506d24821fc89439bc79a89a4eea9ffb5c903e9d7bc1c427d3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.12.11%2B20250702-x86_64_v4-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "da76b72b295b230022a839d42edfde36f79ebfd70c9b381f6ed551066c3942bd", "variant": null }, "cpython-3.12.11-windows-aarch64-none": { @@ -10859,8 +10859,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-aarch64-pc-windows-msvc-install_only_stripped.tar.gz", - "sha256": "45e121ad8e82eb9204cc1da53d0fdf125923f7caed18bf282b3c00dcfae89486", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.12.11%2B20250702-aarch64-pc-windows-msvc-install_only_stripped.tar.gz", + "sha256": "b5e56ebce5ea3cc0add5e460a254da1e095fdcf962552dceea1be314c45115bf", "variant": null }, "cpython-3.12.11-windows-i686-none": { @@ -10875,8 +10875,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-i686-pc-windows-msvc-install_only_stripped.tar.gz", - "sha256": "d80c67dd88aca4c94a57a8b2fe63de524a9e1f2308e5ecd2ca83a7961b9b9b17", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.12.11%2B20250702-i686-pc-windows-msvc-install_only_stripped.tar.gz", + "sha256": "0db0a69bab9aa6159f62d99074918b67e2a81c84b445570befeb583053663b58", "variant": null }, "cpython-3.12.11-windows-x86_64-none": { @@ -10891,8 +10891,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-x86_64-pc-windows-msvc-install_only_stripped.tar.gz", - "sha256": "91f7196d5d103eb3d1214f74606144adb326f0770d80c636cb11b96670477736", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.12.11%2B20250702-x86_64-pc-windows-msvc-install_only_stripped.tar.gz", + "sha256": "195033883da90a35a57aecce522eb068b9b0a36908e6e07b91929f0acf646c8f", "variant": null }, "cpython-3.12.11+debug-linux-aarch64-gnu": { @@ -10907,8 +10907,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-aarch64-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "70a49e31e1f15036715e67ec08ef63df321171c7b922f16db9b462ed9d1ccbc6", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.12.11%2B20250702-aarch64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "b0f04667686f489a410bb3e641b6abefa75dad033cd6d2725ab49a40413e15b7", "variant": "debug" }, "cpython-3.12.11+debug-linux-armv7-gnueabi": { @@ -10923,8 +10923,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-armv7-unknown-linux-gnueabi-debug-full.tar.zst", - "sha256": "87f27b3efb66a226f7a03029f14cbabdc512974f8489b003cb848dec3fde869c", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.12.11%2B20250702-armv7-unknown-linux-gnueabi-debug-full.tar.zst", + "sha256": "d882d16f304b858173b40cca5c681e8f9c9f13774c26390303bd7e7657a1d73c", "variant": "debug" }, "cpython-3.12.11+debug-linux-armv7-gnueabihf": { @@ -10939,8 +10939,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-armv7-unknown-linux-gnueabihf-debug-full.tar.zst", - "sha256": "28a431937f2b5467a0e74425d64bf27ca2e0c37681e3e93f71c19386cf457237", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.12.11%2B20250702-armv7-unknown-linux-gnueabihf-debug-full.tar.zst", + "sha256": "5934f6214d60a958fa3cb3147dad1941d912e0a9f37280de911cbf51a2a231be", "variant": "debug" }, "cpython-3.12.11+debug-linux-powerpc64le-gnu": { @@ -10955,8 +10955,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-ppc64le-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "e08f6f7faabda320f0a38a3388056f29641d7375a6247a6aebf885907a3baf2b", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.12.11%2B20250702-ppc64le-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "a2c821f4a83c3a80d8ec25cf3ca5380aa749488d87db5c99f1c3100069309f5f", "variant": "debug" }, "cpython-3.12.11+debug-linux-riscv64-gnu": { @@ -10971,8 +10971,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-riscv64-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "3537f9e61a46d50bc500310c0056a7b99d08a09b5ba29142c783d226a7712c4e", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.12.11%2B20250702-riscv64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "d1ac376b8756a057ba0d885171caa72bc7cd7ab7436ebc93bd7c0c47cff01d05", "variant": "debug" }, "cpython-3.12.11+debug-linux-s390x-gnu": { @@ -10987,8 +10987,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-s390x-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "859d643152c2b86621a6aa9cfb9d1f50654b402e852ce9cdc864a2f07652c4f7", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.12.11%2B20250702-s390x-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "8a57e27c920d1c93d45a3703c9f3fe047bac6305660a6d5ce2df51b0f7cfef26", "variant": "debug" }, "cpython-3.12.11+debug-linux-x86_64-gnu": { @@ -11003,8 +11003,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-x86_64-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "59aaff170ce2a6efe4a34e5ed50db83c4e29951e3c83ac7fa031f4b8247a440e", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.12.11%2B20250702-x86_64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "6b1e42f30f027dc793f6edaf35c2ff857c63b0f72278c886917e99b6edd064b1", "variant": "debug" }, "cpython-3.12.11+debug-linux-x86_64-musl": { @@ -11019,8 +11019,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-x86_64-unknown-linux-musl-debug-full.tar.zst", - "sha256": "9b43f05f37cd39ff45021d8a13b219cb6ad970130c1c32463cd563002aaff9a7", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.12.11%2B20250702-x86_64-unknown-linux-musl-debug-full.tar.zst", + "sha256": "62b9039f765e56538de58cb37be6baaf2d9da35bb6d95c5e734b432ccec474f8", "variant": "debug" }, "cpython-3.12.11+debug-linux-x86_64_v2-gnu": { @@ -11035,8 +11035,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "37ba61db27c6a1f9d540db6404ea2dfe1f0a221d08332b35476e2d88dd517a0c", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.12.11%2B20250702-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "e1fb28c54a76f4e91f4d53fd5fd1840c7f0045049f7fca29f59c4d7bdfa8134d", "variant": "debug" }, "cpython-3.12.11+debug-linux-x86_64_v2-musl": { @@ -11051,8 +11051,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-x86_64_v2-unknown-linux-musl-debug-full.tar.zst", - "sha256": "1c1fc0c7185caf9eaa5914b00176cbf9a2c4bc6922e6dde52587f3125c8f9db4", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.12.11%2B20250702-x86_64_v2-unknown-linux-musl-debug-full.tar.zst", + "sha256": "c8d4fc92c668c0455a3dce10b2c107651a0d0676e449d30f2d4b6bb3cf2dac1d", "variant": "debug" }, "cpython-3.12.11+debug-linux-x86_64_v3-gnu": { @@ -11067,8 +11067,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "2c938382641e5d73c4b148396289f5c047ddbaa9c765ec541983c212e1e02407", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.12.11%2B20250702-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "6cd111fa008f0a30345d0475e53f99148dc1aab3a39af73b7029ef4fc17c2717", "variant": "debug" }, "cpython-3.12.11+debug-linux-x86_64_v3-musl": { @@ -11083,8 +11083,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-x86_64_v3-unknown-linux-musl-debug-full.tar.zst", - "sha256": "ca3e3795de99768fef314cd40c9b1afe52a4ee6e1409121e8b8d84f524043716", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.12.11%2B20250702-x86_64_v3-unknown-linux-musl-debug-full.tar.zst", + "sha256": "3f4595219aaa4b55f3169f71895bac0f63563a2e27c3653ba5008249d7eb4ed0", "variant": "debug" }, "cpython-3.12.11+debug-linux-x86_64_v4-gnu": { @@ -11099,8 +11099,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "6c8cfad74f3ff99f275717096401021a15b92939fac1ce2ba0803d78beb8fa45", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.12.11%2B20250702-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "8976e1ef981ac4ceb186cb9bf367c279361060f468237a191f2ca2e52fd7a08b", "variant": "debug" }, "cpython-3.12.11+debug-linux-x86_64_v4-musl": { @@ -11115,8 +11115,8 @@ "minor": 12, "patch": 11, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.12.11%2B20250630-x86_64_v4-unknown-linux-musl-debug-full.tar.zst", - "sha256": "f4ec5b942fc2ddf55f26ec6a25b2ddfcda94fe095e158582a8f523258ee09cc4", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.12.11%2B20250702-x86_64_v4-unknown-linux-musl-debug-full.tar.zst", + "sha256": "9008ed69a57d76a2e23b6119603217782a8ea3d30efebb550292348223ca87a5", "variant": "debug" }, "cpython-3.12.10-darwin-aarch64-none": { @@ -15163,8 +15163,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-aarch64-apple-darwin-install_only_stripped.tar.gz", - "sha256": "89079bf9233e4305fac31fceef3e11f955ab78e3e3b0eedd8dabda39ca21558d", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.11.13%2B20250702-aarch64-apple-darwin-install_only_stripped.tar.gz", + "sha256": "7b32a1368af181ef16b2739f65849bb74d4ef1f324613ad9022d6f6fe3bb25f0", "variant": null }, "cpython-3.11.13-darwin-x86_64-none": { @@ -15179,8 +15179,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-x86_64-apple-darwin-install_only_stripped.tar.gz", - "sha256": "441c0ef2ed9ee20d762c489dc3f2489d53d5a2b811af675fec2c0786273dd301", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.11.13%2B20250702-x86_64-apple-darwin-install_only_stripped.tar.gz", + "sha256": "7e9a250b61d7c5795dfe564f12869bef52898612220dfda462da88cdcf20031c", "variant": null }, "cpython-3.11.13-linux-aarch64-gnu": { @@ -15195,8 +15195,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "7d6fb24f7d81af6a89d1a4358cc007ed513747c5beda578fb62a41e139ce0035", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.11.13%2B20250702-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "e39b0b3d68487020cbd90e8ab66af334769059b9d4200901da6a6d0af71a0033", "variant": null }, "cpython-3.11.13-linux-armv7-gnueabi": { @@ -15211,8 +15211,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-armv7-unknown-linux-gnueabi-install_only_stripped.tar.gz", - "sha256": "cb04fcda5b56cc274893e85f01ce536e5cc540c43352fc47f8b66280ffa1afaa", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.11.13%2B20250702-armv7-unknown-linux-gnueabi-install_only_stripped.tar.gz", + "sha256": "48c8d43677ffbdff82c286c8a3afb472eba533070f2e064c7d9df9cbb2f6decf", "variant": null }, "cpython-3.11.13-linux-armv7-gnueabihf": { @@ -15227,8 +15227,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-armv7-unknown-linux-gnueabihf-install_only_stripped.tar.gz", - "sha256": "542cd5b270c16f841993fb63ecdb8ab3d85d6087cfef929304011949f3b6902e", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.11.13%2B20250702-armv7-unknown-linux-gnueabihf-install_only_stripped.tar.gz", + "sha256": "51e2914bb9846c2d88da69043371250f1fb7c1cafbc511d34794dbec5052cf98", "variant": null }, "cpython-3.11.13-linux-powerpc64le-gnu": { @@ -15243,8 +15243,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-ppc64le-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "fa361259ac188fb6ee21c6928acfbb0ae287af5c5acbb01c8c55880d72d53d22", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.11.13%2B20250702-ppc64le-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "be21c281b42b4fc250337111f8867d4cc7ced4f409303cc8dd5a56c6c6a820c7", "variant": null }, "cpython-3.11.13-linux-riscv64-gnu": { @@ -15259,8 +15259,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-riscv64-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "23970dd65b2481eb8c7ddd211ba9841f7eb0927a9a3107a520a75ca97be3df3b", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.11.13%2B20250702-riscv64-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "ab9d02b521ca79f82f25391e84f35f0a984b949da088091f65744fcf9a83add9", "variant": null }, "cpython-3.11.13-linux-s390x-gnu": { @@ -15275,8 +15275,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-s390x-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "c0d3c3a102df12839c7632fb82e2210ccc0045c7b6cc3fc355e0fda30a9e6745", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.11.13%2B20250702-s390x-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "2ab4713ea357a1da9da835712ea73b40aa93fe7f55528366e10ea61d8edb4bd0", "variant": null }, "cpython-3.11.13-linux-x86_64-gnu": { @@ -15291,8 +15291,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "07c15c63bdf6d14d80a50ec3ed9a0e81d04b9cf9671decfdec3d508ab252a713", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.11.13%2B20250702-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "096a6301b7029d11db44b1fd4364a3d474a3f5c7f2cd9317521bc58abf40b990", "variant": null }, "cpython-3.11.13-linux-x86_64-musl": { @@ -15307,8 +15307,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-x86_64-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "224de8d55c04df9cbf9555b57810dc6f87af82f0902c0883fcf6ed164c99b2cb", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.11.13%2B20250702-x86_64-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "7aba64af498dfc9056405e21d5857ebf7e2dc88550de2f9b97efc5d67f100d18", "variant": null }, "cpython-3.11.13-linux-x86_64_v2-gnu": { @@ -15323,8 +15323,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-x86_64_v2-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "37bc8eb56569dcb033efb69ffcb3a7dcc19220c9b6c88a47e2df1a4bcbc5bce3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.11.13%2B20250702-x86_64_v2-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "806db935974b0d1c442c297fcb9e9d87b692e8f81bd4d887927449bb7eef70bf", "variant": null }, "cpython-3.11.13-linux-x86_64_v2-musl": { @@ -15339,8 +15339,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-x86_64_v2-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "d64c66b290eff19f4c200f07553f80e6d61afdb3d834b3ac989cda21731f2f59", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.11.13%2B20250702-x86_64_v2-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "e1dfc3b7064af3cbc68b95bdefcb88178fa9b3493f2a276b5b8e8610440ad9f3", "variant": null }, "cpython-3.11.13-linux-x86_64_v3-gnu": { @@ -15355,8 +15355,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-x86_64_v3-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "3099ad95267bd3154697079c1c057e84e206bdfc6cdb728f0d47c824f82db260", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.11.13%2B20250702-x86_64_v3-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "c429bb26841da0b104644c1ab11dc8a76863e107436ad06e806f6bb54f7ec126", "variant": null }, "cpython-3.11.13-linux-x86_64_v3-musl": { @@ -15371,8 +15371,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-x86_64_v3-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "231358a2adc7a132c07470577dfffa23353aca7384c1af6e87d0174e859df133", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.11.13%2B20250702-x86_64_v3-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "c618c57d50dd9bdd0f989d71dec9b76a742b051c1ae94111ca15515e183f87ee", "variant": null }, "cpython-3.11.13-linux-x86_64_v4-gnu": { @@ -15387,8 +15387,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-x86_64_v4-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "27c9bec499de6ff14909f030c2d788e41e69d5b5ee2b9deb1f78666f630d6da4", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.11.13%2B20250702-x86_64_v4-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "c1cf678915eb527b824464841c2c56508836bf8595778f39a9bbb7975d59806d", "variant": null }, "cpython-3.11.13-linux-x86_64_v4-musl": { @@ -15403,8 +15403,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-x86_64_v4-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "0d7bffc0778796def75b663e31c138592237cd205e358540b4bafd43df109c09", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.11.13%2B20250702-x86_64_v4-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "d527c8614340ac46905969ac80e2c62327b7e987fbd448cfd74d66578ab42c67", "variant": null }, "cpython-3.11.13-windows-aarch64-none": { @@ -15419,8 +15419,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-aarch64-pc-windows-msvc-install_only_stripped.tar.gz", - "sha256": "e5d8f83e8728506ad08382c7f68328b4204dd52c3f9cb4337087b1f21b68ee2a", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.11.13%2B20250702-aarch64-pc-windows-msvc-install_only_stripped.tar.gz", + "sha256": "3af266f887d83e4705e2ceb2eb7d770f9c74454d676e739e768097d3ff9dc148", "variant": null }, "cpython-3.11.13-windows-i686-none": { @@ -15435,8 +15435,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-i686-pc-windows-msvc-install_only_stripped.tar.gz", - "sha256": "228e47684fe989db07085f32b8dca766027ba34672252e9140f5bd367e0ba23f", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.11.13%2B20250702-i686-pc-windows-msvc-install_only_stripped.tar.gz", + "sha256": "b38912438477ed7a7cb69aa92a5a834ffbb88d8fa5026eb626f1530adb3e00c7", "variant": null }, "cpython-3.11.13-windows-x86_64-none": { @@ -15451,8 +15451,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-x86_64-pc-windows-msvc-install_only_stripped.tar.gz", - "sha256": "abde9c8b8b1230f1a21b52efd2e41a43b5af0a754255d294e180238bf6d291e0", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.11.13%2B20250702-x86_64-pc-windows-msvc-install_only_stripped.tar.gz", + "sha256": "edb3eb9c646997de50b27932fdf10d8853614bdbd7d651c686459fc227776c1a", "variant": null }, "cpython-3.11.13+debug-linux-aarch64-gnu": { @@ -15467,8 +15467,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-aarch64-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "c07c910236c121818decbdfd947f5033741afc8610f347256179cbda9cee0ccf", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.11.13%2B20250702-aarch64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "5a58a85c773dcfd33b88b345fc899ab983e689fe5bf5ca6682fe62d1f3b65694", "variant": "debug" }, "cpython-3.11.13+debug-linux-armv7-gnueabi": { @@ -15483,8 +15483,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-armv7-unknown-linux-gnueabi-debug-full.tar.zst", - "sha256": "2b15db81177c7975393cbae2a5b7989b59318084c0e8ae3798cb5982606bf5d1", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.11.13%2B20250702-armv7-unknown-linux-gnueabi-debug-full.tar.zst", + "sha256": "6b24708d696e86792db8214cb20d7c1bd9a0d03f926542cde7a5251a466977d8", "variant": "debug" }, "cpython-3.11.13+debug-linux-armv7-gnueabihf": { @@ -15499,8 +15499,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-armv7-unknown-linux-gnueabihf-debug-full.tar.zst", - "sha256": "b3f948f00201905cf6a7448030a40a51213b3f937f29f501114354f4cc419886", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.11.13%2B20250702-armv7-unknown-linux-gnueabihf-debug-full.tar.zst", + "sha256": "ddd27f58a436b31bf1a3f39d53c670ab0ed481f677b1602d5fb0a5a89f471069", "variant": "debug" }, "cpython-3.11.13+debug-linux-powerpc64le-gnu": { @@ -15515,8 +15515,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-ppc64le-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "7f2f6ceb4f242e71977eefd5e0e702c5e87d6600342d79e6dadaf2e59ed5476f", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.11.13%2B20250702-ppc64le-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "5306fced1a898247f9e3cc897a28f05b647d8b70ed3ece80ea9f7fa525459d94", "variant": "debug" }, "cpython-3.11.13+debug-linux-riscv64-gnu": { @@ -15531,8 +15531,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-riscv64-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "7454ccaae24cf8742ea55ac74b0b1d147be3f99bf290c7c5a02f0027a36805ac", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.11.13%2B20250702-riscv64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "fee8c8cb156c0aa84f41759b277bc27c6ce004c1bbfd03a202c8b0347ea29e50", "variant": "debug" }, "cpython-3.11.13+debug-linux-s390x-gnu": { @@ -15547,8 +15547,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-s390x-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "9fa9c461b14dd142e15cf95db1b0ca7310ea603fec250eb1952a8d31b64027f0", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.11.13%2B20250702-s390x-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "eff457ef514ffaf954fa2bfd63fde5fc128a908e5a0d72fe8dab0e4146867f54", "variant": "debug" }, "cpython-3.11.13+debug-linux-x86_64-gnu": { @@ -15563,8 +15563,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-x86_64-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "533d8ccdb77fc43847227850361809c9bfb85223a08d689e31087df34d8260ac", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.11.13%2B20250702-x86_64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "23c7d6c58a3e9eb0055b847a72053082e1250b04c39ee0026738d0a2298d6dbb", "variant": "debug" }, "cpython-3.11.13+debug-linux-x86_64-musl": { @@ -15579,8 +15579,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-x86_64-unknown-linux-musl-debug-full.tar.zst", - "sha256": "0f4dfe78c30d56acb5e6df28de4058177ac188ddd6ea1f2d3a18db7fcfe7ff1b", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.11.13%2B20250702-x86_64-unknown-linux-musl-debug-full.tar.zst", + "sha256": "c51dbba70ae11f65a0399d5690a4c1fbb52d9772fc8b1467ed836247225db3af", "variant": "debug" }, "cpython-3.11.13+debug-linux-x86_64_v2-gnu": { @@ -15595,8 +15595,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "03a7b763a2e5deec621813a9081858aad3ed0f24368ff94bf4a78eb652ffa8a0", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.11.13%2B20250702-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "479bc0f7b9bae4dde42ec848e508ecd8095f28ee4e89ef1f18e95ec2e29aa19d", "variant": "debug" }, "cpython-3.11.13+debug-linux-x86_64_v2-musl": { @@ -15611,8 +15611,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-x86_64_v2-unknown-linux-musl-debug-full.tar.zst", - "sha256": "b5e1e4c7fc8a3eff8a85f4b4dd004f7d835a8ac79a982da65054d605f106c5eb", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.11.13%2B20250702-x86_64_v2-unknown-linux-musl-debug-full.tar.zst", + "sha256": "a25ddc1e2588842ada52fdf4211939d5e598defd3d45702ec0d9dfa30797060a", "variant": "debug" }, "cpython-3.11.13+debug-linux-x86_64_v3-gnu": { @@ -15627,8 +15627,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "d020b9ad0fb3afc95ccf6c9737092f8ea4e977d8f6c0bd2503cd60457113fbfa", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.11.13%2B20250702-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "d9f819fe8cbd7895c9b9d591e55ca67b500875c945cc0a1278149267d8cdd803", "variant": "debug" }, "cpython-3.11.13+debug-linux-x86_64_v3-musl": { @@ -15643,8 +15643,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-x86_64_v3-unknown-linux-musl-debug-full.tar.zst", - "sha256": "2f04db73b5fb332f37e409ec048e09a60fd1da729893f629ae37c4991f354d0e", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.11.13%2B20250702-x86_64_v3-unknown-linux-musl-debug-full.tar.zst", + "sha256": "dd22dd11e9bc4bbc716c1af20885c01a3d032eb1ce7bb74f9f939f6a08545ddc", "variant": "debug" }, "cpython-3.11.13+debug-linux-x86_64_v4-gnu": { @@ -15659,8 +15659,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "8a5ea7c66a05947522d528cb47e03dd795ab108e78b0926039119e9f8341e5b0", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.11.13%2B20250702-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "ab7171c7e0dcfdf7135aaed53169e71222cddc8c4133b7d51f898842bb924f0e", "variant": "debug" }, "cpython-3.11.13+debug-linux-x86_64_v4-musl": { @@ -15675,8 +15675,8 @@ "minor": 11, "patch": 13, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.11.13%2B20250630-x86_64_v4-unknown-linux-musl-debug-full.tar.zst", - "sha256": "9829333c2b776ff11ab2e4be31237dc6c36cb2e13c04c361c7c57f4d54c6af3b", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.11.13%2B20250702-x86_64_v4-unknown-linux-musl-debug-full.tar.zst", + "sha256": "c02f0ef29ce93442ac3a61bbf3560c24d74d34b8edb46b166724ff139cde8f26", "variant": "debug" }, "cpython-3.11.12-darwin-aarch64-none": { @@ -19467,8 +19467,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-aarch64-apple-darwin-install_only_stripped.tar.gz", - "sha256": "c34bfae5fe85a28c1b13c2d160268eafee18b66f25c29b139958e164897a2527", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.10.18%2B20250702-aarch64-apple-darwin-install_only_stripped.tar.gz", + "sha256": "4ad4b0c3b60c14750fb8d0ad758829cd1a54df318dc6d83c239c279443bb854c", "variant": null }, "cpython-3.10.18-darwin-x86_64-none": { @@ -19483,8 +19483,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-x86_64-apple-darwin-install_only_stripped.tar.gz", - "sha256": "f2676d7b52e94334c4564c441c0aeabda9858f29384cbaf824c8a091b574f938", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.10.18%2B20250702-x86_64-apple-darwin-install_only_stripped.tar.gz", + "sha256": "9c9833f4f13eed050a1440204b0477d652ae76c8e749bc26021928d5c6fcba2b", "variant": null }, "cpython-3.10.18-linux-aarch64-gnu": { @@ -19499,8 +19499,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "b6f3243caa1fdcf12d62e1711e8a8255ae419632e1e98b6b1fa9809564af82f0", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.10.18%2B20250702-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "6c315a5ed0b77457c494048269e71e36e0fae2a9354da0bbfc65f3d583a306fa", "variant": null }, "cpython-3.10.18-linux-armv7-gnueabi": { @@ -19515,8 +19515,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-armv7-unknown-linux-gnueabi-install_only_stripped.tar.gz", - "sha256": "c091409b05c986623be140e87bedea2a7f2d005dbf1564b9b529d3b3727f5608", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.10.18%2B20250702-armv7-unknown-linux-gnueabi-install_only_stripped.tar.gz", + "sha256": "c0aa7dfaef03330a1009fae6ed3696062a9c6b6a879de57643222911801f6b14", "variant": null }, "cpython-3.10.18-linux-armv7-gnueabihf": { @@ -19531,8 +19531,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-armv7-unknown-linux-gnueabihf-install_only_stripped.tar.gz", - "sha256": "326c378407486a185c516d927652978728f056af6d62150c70678f6b590c1df9", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.10.18%2B20250702-armv7-unknown-linux-gnueabihf-install_only_stripped.tar.gz", + "sha256": "6a772781facf097fb9bb00fc16b9c77680fc583dbb04ef4f935f1139f5a3a818", "variant": null }, "cpython-3.10.18-linux-powerpc64le-gnu": { @@ -19547,8 +19547,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-ppc64le-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "3ec27bd1f7f428fb12d9973d84fe268eca476097ab3ab650b4b73fd21861832a", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.10.18%2B20250702-ppc64le-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "8bbc7cd369d3c3788ca46a66b0c9f0d227054f99b7db3966a547faa7e0ede99c", "variant": null }, "cpython-3.10.18-linux-riscv64-gnu": { @@ -19563,8 +19563,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-riscv64-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "9da0a695abfd3a18355642055f79bc0d227e05c03d0430c308e02143fc7dbf9d", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.10.18%2B20250702-riscv64-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "954603b1e72f7b697812bb821b9820f2d1ab21b9fb166201c068df28577f3967", "variant": null }, "cpython-3.10.18-linux-s390x-gnu": { @@ -19579,8 +19579,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-s390x-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "8192b48982c0f0c0ff642bf84bcf26003812eaac5ef4853ba9b7b6e80745923a", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.10.18%2B20250702-s390x-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "8d02a604f4ef13541a678b8f32b2955003f53471489460f867de3bbbd0b7b0a2", "variant": null }, "cpython-3.10.18-linux-x86_64-gnu": { @@ -19595,8 +19595,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "11b27504b26ea1977bf7b7e6ef6c9da4d648b78707fa34fe0538f08744978a4b", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.10.18%2B20250702-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "780e5199279301cec595590e1a12549e615f5863e794db171897b996deb5db2b", "variant": null }, "cpython-3.10.18-linux-x86_64-musl": { @@ -19611,8 +19611,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-x86_64-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "f5302f08043468f9c9a47fa7884393c27de66b19b3e46096984245d8f4d8be8f", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.10.18%2B20250702-x86_64-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "fac56a0d55b28dfada5b1b1ad12c38bca7fda14621f84d4dba599dfb697d0f6a", "variant": null }, "cpython-3.10.18-linux-x86_64_v2-gnu": { @@ -19627,8 +19627,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-x86_64_v2-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "e979cf0596697fc729d42c5d6d37e1d591374ac56c42d1b5db9e05750984f122", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.10.18%2B20250702-x86_64_v2-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "b47af0eb09bd0ae5d5b33e0bfd3a121dd8bf042ffe61d03d54be27629db55a78", "variant": null }, "cpython-3.10.18-linux-x86_64_v2-musl": { @@ -19643,8 +19643,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-x86_64_v2-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "d9d0c7e28bc0f5f2a8ce0a9f4d57f8fe263361f5fd7d8afd6f8eeecc7ce00f96", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.10.18%2B20250702-x86_64_v2-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "c59eac8b665419cc94c24807fd2654cc424f7f926a6b107a7e22a9599ba416ea", "variant": null }, "cpython-3.10.18-linux-x86_64_v3-gnu": { @@ -19659,8 +19659,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-x86_64_v3-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "3438a6066eb9f801f5a3153d7ee5fb0d1bf7769e44a610effd2340e846a9fd44", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.10.18%2B20250702-x86_64_v3-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "f2042831ec67633ad96f27407fee67b671bb5a589c8c8491dbb9420f58246db8", "variant": null }, "cpython-3.10.18-linux-x86_64_v3-musl": { @@ -19675,8 +19675,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-x86_64_v3-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "ae4eeba3037b6810eb9e96814d3980e28a5a3de5277f470100742a48391c6df7", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.10.18%2B20250702-x86_64_v3-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "2205ef12cd51afe189ac152b9413788eccc5e0d8c86b78f6c2209ab8d5ead0b8", "variant": null }, "cpython-3.10.18-linux-x86_64_v4-gnu": { @@ -19691,8 +19691,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-x86_64_v4-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "c8cde20b14e0ef0e564d5a1cbd0c6003ae178dfea9d446cbf3ee15d75ac8a9d8", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.10.18%2B20250702-x86_64_v4-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "f80c94a23c67b2cd7695fb58d3dd3bb4509cbe94bf3da9200dcc7f5c06939067", "variant": null }, "cpython-3.10.18-linux-x86_64_v4-musl": { @@ -19707,8 +19707,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-x86_64_v4-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "46da91101e88f68f5fa21a2aeadeeb91072cbe9d9f1caa08e34481180ec3dea3", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.10.18%2B20250702-x86_64_v4-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "2356bc9f121cb555921a10155126b53ca92e471e35e93644feae37ef6adbe91d", "variant": null }, "cpython-3.10.18-windows-i686-none": { @@ -19723,8 +19723,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-i686-pc-windows-msvc-install_only_stripped.tar.gz", - "sha256": "faab88604275a0c869cf3fb19f63b0158bd8356d74fff15ebd05114908683fb1", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.10.18%2B20250702-i686-pc-windows-msvc-install_only_stripped.tar.gz", + "sha256": "16379aad0f72dffdcedc32240bceacf8c341c8ac9c49f1634a94bef3eb34ff91", "variant": null }, "cpython-3.10.18-windows-x86_64-none": { @@ -19739,8 +19739,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-x86_64-pc-windows-msvc-install_only_stripped.tar.gz", - "sha256": "ac56903c7ae726f154b5b000f04db72ddb52775812c638cc67b5090146267502", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.10.18%2B20250702-x86_64-pc-windows-msvc-install_only_stripped.tar.gz", + "sha256": "1d9028a8b16a2dddfd0334a12195eb37653e4ba3dd4691059a58dc18c9c2bad5", "variant": null }, "cpython-3.10.18+debug-linux-aarch64-gnu": { @@ -19755,8 +19755,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-aarch64-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "fcbbf8d8e274f7983a70e104bf708ff21effc28bb594314429317e73960f85ff", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.10.18%2B20250702-aarch64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "ba86c13891bba5395db087bad08e2175d4fe4f7c2592f4228c8302e89b1876ae", "variant": "debug" }, "cpython-3.10.18+debug-linux-armv7-gnueabi": { @@ -19771,8 +19771,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-armv7-unknown-linux-gnueabi-debug-full.tar.zst", - "sha256": "400ab706d0f21b68793379040f9fa96fce9cacfff25217aaa3f37cac8d15fea5", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.10.18%2B20250702-armv7-unknown-linux-gnueabi-debug-full.tar.zst", + "sha256": "da75e3b55503f9cc33e1476e4933457b42c5ac0a765321a8056278056f2c6032", "variant": "debug" }, "cpython-3.10.18+debug-linux-armv7-gnueabihf": { @@ -19787,8 +19787,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-armv7-unknown-linux-gnueabihf-debug-full.tar.zst", - "sha256": "48a8a9cbf0b6a9bfd95af64af97dda167d99240cd3f66265011006f2d2a54e34", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.10.18%2B20250702-armv7-unknown-linux-gnueabihf-debug-full.tar.zst", + "sha256": "65c4d23b2507715b60f8157fda6651ad0490d38d3a354aa5e85c5401f7b791b5", "variant": "debug" }, "cpython-3.10.18+debug-linux-powerpc64le-gnu": { @@ -19803,8 +19803,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-ppc64le-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "8571d2b81798162c1effb57370359fe178d96f3a5a6bb15f5bd88c83caf8c6b4", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.10.18%2B20250702-ppc64le-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "1ea4b070dc8795316798e5dde4a45f9bcbd3b8907ece534e73164e9e82902817", "variant": "debug" }, "cpython-3.10.18+debug-linux-riscv64-gnu": { @@ -19819,8 +19819,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-riscv64-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "dedf8c8bdae3b3d4d8ec028c28bb8ad12f9c78064828426f6570322eba490015", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.10.18%2B20250702-riscv64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "2b9381ee30e69b0a7722a1b0917a4be8abc9b22d3542c918c8810d3bf10144f8", "variant": "debug" }, "cpython-3.10.18+debug-linux-s390x-gnu": { @@ -19835,8 +19835,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-s390x-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "ffd41140c4f55b9700be4381a0ef774067486572156ec17839e38f360512cc3e", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.10.18%2B20250702-s390x-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "3ea8a041ed81fbc11e2781cc6b57ef0abf2ecd874374603153213d316da19e5e", "variant": "debug" }, "cpython-3.10.18+debug-linux-x86_64-gnu": { @@ -19851,8 +19851,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-x86_64-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "ec0b1c02cb0813db9d9bfb1c41633c15c73446d74406d07df0de041dadcd2d7b", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.10.18%2B20250702-x86_64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "afd58d81e22c5f96c7021c27aedb89bc3be3c40d58625035a5b7158bb464a89f", "variant": "debug" }, "cpython-3.10.18+debug-linux-x86_64-musl": { @@ -19867,8 +19867,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-x86_64-unknown-linux-musl-debug-full.tar.zst", - "sha256": "448962b7ac159470130fa32817f917c5db659d5adde6c300c93335fdb0915d92", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.10.18%2B20250702-x86_64-unknown-linux-musl-debug-full.tar.zst", + "sha256": "c82f5cb37140257016a05c92a83813c8ad85f108898c6076750b4bfc8e49052d", "variant": "debug" }, "cpython-3.10.18+debug-linux-x86_64_v2-gnu": { @@ -19883,8 +19883,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "a6d0b714ee3b0b0aae36dbb6c5a28378717c99070797ffda12ddd433a8f8588c", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.10.18%2B20250702-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "fdd72ff3418b1dd74fdc5514d392e309fe615739aafeeeed80276bfa28646e93", "variant": "debug" }, "cpython-3.10.18+debug-linux-x86_64_v2-musl": { @@ -19899,8 +19899,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-x86_64_v2-unknown-linux-musl-debug-full.tar.zst", - "sha256": "09903f2c08fccddce4f7a5d52c2956d898a70a7e47bb07825581e77ad9a77829", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.10.18%2B20250702-x86_64_v2-unknown-linux-musl-debug-full.tar.zst", + "sha256": "d13113baa9f5749b5f70a2e4b396393363df1bba14c4fca6d13455ab92786f16", "variant": "debug" }, "cpython-3.10.18+debug-linux-x86_64_v3-gnu": { @@ -19915,8 +19915,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "c453e8adeed1f0dc8253bbd6ebd72d9803b88fbfd015049b3d281ce915b0dfbf", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.10.18%2B20250702-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "7ac330ff09a193ef7e4a93751dd1bc243a8a2d35debdb9f1f4c967ee98be7c9b", "variant": "debug" }, "cpython-3.10.18+debug-linux-x86_64_v3-musl": { @@ -19931,8 +19931,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-x86_64_v3-unknown-linux-musl-debug-full.tar.zst", - "sha256": "054fdb4b6d27155371cdfa9188a93c4d58325ebb9a061ded8ad7b044cc177064", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.10.18%2B20250702-x86_64_v3-unknown-linux-musl-debug-full.tar.zst", + "sha256": "ffa713da073c0ac6b9d46e8c34f682c936c1ee6ecacfdaa369617d621bc5f800", "variant": "debug" }, "cpython-3.10.18+debug-linux-x86_64_v4-gnu": { @@ -19947,8 +19947,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "0e47c951e3800a10f531ef4eb4d3307822f76674dbe863c11a48547ae51f0e27", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.10.18%2B20250702-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "d22dc14204be742df984cd74b086c5bce23ea6071bbccf66e0a4e9373fb7e1fc", "variant": "debug" }, "cpython-3.10.18+debug-linux-x86_64_v4-musl": { @@ -19963,8 +19963,8 @@ "minor": 10, "patch": 18, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.10.18%2B20250630-x86_64_v4-unknown-linux-musl-debug-full.tar.zst", - "sha256": "7f625673e2d06abac133fd9588b6492c3b6b69b6cacd1b1bb480b9476316ab74", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.10.18%2B20250702-x86_64_v4-unknown-linux-musl-debug-full.tar.zst", + "sha256": "f8b309b55988356eeb43b1d6daaaed44c3f2c7615abb015145e750cc81c84f13", "variant": "debug" }, "cpython-3.10.17-darwin-aarch64-none": { @@ -24907,8 +24907,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-aarch64-apple-darwin-install_only_stripped.tar.gz", - "sha256": "f47a96f6bcf29ef0e803c8524b027c4c1b411d27934e6521ebe3381f9d674f7b", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.9.23%2B20250702-aarch64-apple-darwin-install_only_stripped.tar.gz", + "sha256": "901b88f69f92c93991b03315f6e9853fdf6e57796b7f46eae2578f8e6cec7f79", "variant": null }, "cpython-3.9.23-darwin-x86_64-none": { @@ -24923,8 +24923,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-x86_64-apple-darwin-install_only_stripped.tar.gz", - "sha256": "04fe6e2fd53c9dc4dfbcf870174c44136f82690f7387451bf8863712314eb156", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.9.23%2B20250702-x86_64-apple-darwin-install_only_stripped.tar.gz", + "sha256": "663403464b734f7fcb6697dc03a7bb4415f1bd7c29df8b0476d145e768b6e220", "variant": null }, "cpython-3.9.23-linux-aarch64-gnu": { @@ -24939,8 +24939,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "324e4aaba0d4749d9d9a2007a7fb2e55c6c206241c8607ade29e9a61a70f23c0", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.9.23%2B20250702-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "fd037489d2d0006d9f74f20a751fd0369c61adf2c8ead32c9a572759162b3241", "variant": null }, "cpython-3.9.23-linux-armv7-gnueabi": { @@ -24955,8 +24955,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-armv7-unknown-linux-gnueabi-install_only_stripped.tar.gz", - "sha256": "5f88c66314c7820cb20ca3fef3c191850d0247c78d66cb366590ebb813b36b4d", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.9.23%2B20250702-armv7-unknown-linux-gnueabi-install_only_stripped.tar.gz", + "sha256": "594b85658309561642361b1708aac18579817978ffdbb08f1c5f7040f9c30f28", "variant": null }, "cpython-3.9.23-linux-armv7-gnueabihf": { @@ -24971,8 +24971,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-armv7-unknown-linux-gnueabihf-install_only_stripped.tar.gz", - "sha256": "2a4a348c42b424f5d01ff61882edde283e0135331b54349f5bc41f70282fc56f", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.9.23%2B20250702-armv7-unknown-linux-gnueabihf-install_only_stripped.tar.gz", + "sha256": "40eedb55eda5598dc9335728b70f7dff8b58be111b462e392cf2f8ba249c68ac", "variant": null }, "cpython-3.9.23-linux-powerpc64le-gnu": { @@ -24987,8 +24987,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-ppc64le-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "f80b1a541b74122260372e36504157a94b7b036626779b215f66e26174c16ba1", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.9.23%2B20250702-ppc64le-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "4c294d9bd701ffaa60440e0e1871c5570c690051b7c8f1b674f8e7fc2239e8c9", "variant": null }, "cpython-3.9.23-linux-riscv64-gnu": { @@ -25003,8 +25003,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-riscv64-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "4a57ad0c9a214e97cf18ed366f858b224e7a4c3da8519adcb0f757fb4939c64e", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.9.23%2B20250702-riscv64-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "41c2dd0ab80b4ddd60a22fc775d87bec1e49c533ee0b0aec757e432df17c06ea", "variant": null }, "cpython-3.9.23-linux-s390x-gnu": { @@ -25019,8 +25019,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-s390x-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "f76716832859664fb3bb89708fd60e55cf7bbb9fd4066a1746dda26c9baa633a", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.9.23%2B20250702-s390x-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "530e5bb6e47f5e009768b96d9bed2d0c4fe21f1bc113a35571c6981922dd345f", "variant": null }, "cpython-3.9.23-linux-x86_64-gnu": { @@ -25035,8 +25035,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "a4c3137284a68bcae77e794873978703b079ed3355c86f2d9e3e1e71b2910ccb", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.9.23%2B20250702-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "b11d434321025e814b07e171e17cb183b4fe02bddbec5e036882c85fb7020b18", "variant": null }, "cpython-3.9.23-linux-x86_64-musl": { @@ -25051,8 +25051,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-x86_64-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "a501bfae7b0c8f3fbfd4de76698face42af7f878395b22a7d567c9117f118c43", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.9.23%2B20250702-x86_64-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "996f66c44d75bf681d6d5c5d2f6315b7f0fff9e9e56b628bdf0f4d865be69a31", "variant": null }, "cpython-3.9.23-linux-x86_64_v2-gnu": { @@ -25067,8 +25067,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-x86_64_v2-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "5a609ff46904d28acbc52cfa6b445066ec98ca690ffa8870fdd17537b72d98b0", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.9.23%2B20250702-x86_64_v2-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "9355e74e4922c9ffd62fadfd0d8949a1de860c14ad16db8ec80e04552219eeaa", "variant": null }, "cpython-3.9.23-linux-x86_64_v2-musl": { @@ -25083,8 +25083,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-x86_64_v2-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "a041d91af09e4246f463361ec76de06080fd1cf05771f59e13a1fbbc42fb3d6f", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.9.23%2B20250702-x86_64_v2-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "3548ddd479dc2ca6d108cba69c0e267a37664ff795d7ebc908836a3faacef9b1", "variant": null }, "cpython-3.9.23-linux-x86_64_v3-gnu": { @@ -25099,8 +25099,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-x86_64_v3-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "b7bd686ea5e00697498baead5a01abe0ceb4a0c9e6fbed5823b0c8225fb25176", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.9.23%2B20250702-x86_64_v3-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "4364cf01c55eee28f5ca918cc9c20f3130cec3d20c45156623576986729e7a9f", "variant": null }, "cpython-3.9.23-linux-x86_64_v3-musl": { @@ -25115,8 +25115,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-x86_64_v3-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "4daf37cac1f2fcc6696c64f0a9705f01121046f9b4a3c5424b83714a61161a5b", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.9.23%2B20250702-x86_64_v3-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "ac0f0cca348f51d29c3e9201e8cb35251b0eceb0e6d29ce2b652fc2bd912bf7c", "variant": null }, "cpython-3.9.23-linux-x86_64_v4-gnu": { @@ -25131,8 +25131,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-x86_64_v4-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "4cf404087df0b1deb8332725583789078b868b5baa6aa5375e168d3cdb987e7f", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.9.23%2B20250702-x86_64_v4-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "4622c9b7aad91c6aa9d3b251a5721b52725866defb6132e9d8b0c7b05ebdd317", "variant": null }, "cpython-3.9.23-linux-x86_64_v4-musl": { @@ -25147,8 +25147,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-x86_64_v4-unknown-linux-musl-install_only_stripped.tar.gz", - "sha256": "f7e827cc26ce8051e77ec7a7bea2d658a1f924664e0725c21a67ab15d6f77bf8", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.9.23%2B20250702-x86_64_v4-unknown-linux-musl-install_only_stripped.tar.gz", + "sha256": "227544768f4214350a1051282a49e598a742bead5447ac7adfb1da488cf6b165", "variant": null }, "cpython-3.9.23-windows-i686-none": { @@ -25163,8 +25163,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-i686-pc-windows-msvc-install_only_stripped.tar.gz", - "sha256": "09e4db6bd06c7b26e437df662fda4d507f276ee1ba90b986e5596a823f5d980b", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.9.23%2B20250702-i686-pc-windows-msvc-install_only_stripped.tar.gz", + "sha256": "0029b916ac37b330d40c6fa13f507249660f0ceaaa34415bc691e705925b6d1b", "variant": null }, "cpython-3.9.23-windows-x86_64-none": { @@ -25179,8 +25179,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-x86_64-pc-windows-msvc-install_only_stripped.tar.gz", - "sha256": "3c50ad97c45bcf0b2984479ebe896f61032222605d317ad059a481730a0ee72a", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.9.23%2B20250702-x86_64-pc-windows-msvc-install_only_stripped.tar.gz", + "sha256": "fd864f5f2aff6727250bd9104409a458146552f88d6ae7b78427aed719506b9c", "variant": null }, "cpython-3.9.23+debug-linux-aarch64-gnu": { @@ -25195,8 +25195,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-aarch64-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "751f68beb0dd5067bde56d9a4028b0233e2a71f84bf421e8627dc2abd068d954", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.9.23%2B20250702-aarch64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "080edc8aca719b776e62e1803948390cc75392db8a416f3ebc3fa1b6ec219c8e", "variant": "debug" }, "cpython-3.9.23+debug-linux-armv7-gnueabi": { @@ -25211,8 +25211,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-armv7-unknown-linux-gnueabi-debug-full.tar.zst", - "sha256": "29ee738ac4d186bb431c4382a6d1cc550a0916a1ce99d58769fc5371faff8409", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.9.23%2B20250702-armv7-unknown-linux-gnueabi-debug-full.tar.zst", + "sha256": "b187d469dd3c61efdac4ac4a9f9a17e01db860bef5e836251ad38e751bd2f2e9", "variant": "debug" }, "cpython-3.9.23+debug-linux-armv7-gnueabihf": { @@ -25227,8 +25227,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-armv7-unknown-linux-gnueabihf-debug-full.tar.zst", - "sha256": "afea4baf253f4bf91781c122308ca306d72fa1e76526abf2b78648772c979d2c", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.9.23%2B20250702-armv7-unknown-linux-gnueabihf-debug-full.tar.zst", + "sha256": "d4f4ae11a45a4f7821caca519880fe79a052bb8191cbc7678965304d5efea5a3", "variant": "debug" }, "cpython-3.9.23+debug-linux-powerpc64le-gnu": { @@ -25243,8 +25243,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-ppc64le-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "0c29b50f57489b93b29a6fe00cb24fb84cd6713c065e35794d1d29d81485c01d", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.9.23%2B20250702-ppc64le-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "2c389fc71513a2f75ef3a1a299a160d1a7d19f701f2a9635ece77454b2fddfb1", "variant": "debug" }, "cpython-3.9.23+debug-linux-riscv64-gnu": { @@ -25259,8 +25259,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-riscv64-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "e4c17ce1f909fe47049fb3236301dde356d4ccc456f70e59e31df8f6b6e21f7d", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.9.23%2B20250702-riscv64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "4c11855610bfe76f7dd003bcf3be0df9f656a41667c835df9da870b8ee91c465", "variant": "debug" }, "cpython-3.9.23+debug-linux-s390x-gnu": { @@ -25275,8 +25275,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-s390x-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "8f2783d56728a918568f8ec81d2b7c8b0e4060e605f42b4ae50b9f127925b54c", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.9.23%2B20250702-s390x-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "90d4077a0907f4e491662b92184368b6b16f4b3623e618fdbd37ae6ceecb6813", "variant": "debug" }, "cpython-3.9.23+debug-linux-x86_64-gnu": { @@ -25291,8 +25291,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-x86_64-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "f4ef26643dfb13a80439373ce4e192f9c678ccffce8bda9455258052c29e0c85", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.9.23%2B20250702-x86_64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "5a062251e9ee9f765373cb5eae61943bc214f8363392e3cffd235ca1a751ef98", "variant": "debug" }, "cpython-3.9.23+debug-linux-x86_64-musl": { @@ -25307,8 +25307,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-x86_64-unknown-linux-musl-debug-full.tar.zst", - "sha256": "f74953fe519fbdbb35ce722c97521b89c915da7d630af5a82911dd352f5c8cec", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.9.23%2B20250702-x86_64-unknown-linux-musl-debug-full.tar.zst", + "sha256": "e5e5ef74bd58d9f0994e583830811ec3be9149276a1434753b07bd19d77e9417", "variant": "debug" }, "cpython-3.9.23+debug-linux-x86_64_v2-gnu": { @@ -25323,8 +25323,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "039ada74a3fea0f359a617a57e83ee92c43d5770175bb3c26d96ac65364f7f5c", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.9.23%2B20250702-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "3fc2ad7307cd0fb5e360baea3b598ed9218313f51f83063b4d085fcf6c85c7e0", "variant": "debug" }, "cpython-3.9.23+debug-linux-x86_64_v2-musl": { @@ -25339,8 +25339,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-x86_64_v2-unknown-linux-musl-debug-full.tar.zst", - "sha256": "64e72ce5b13bff5f7f9e44032ecf6ff70212b7384d2512ab5ed8b6edddd90e68", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.9.23%2B20250702-x86_64_v2-unknown-linux-musl-debug-full.tar.zst", + "sha256": "ec069be5c7b2705b885993ed8f15f3e0456f445beeee1f372b65fdd89afc7cd1", "variant": "debug" }, "cpython-3.9.23+debug-linux-x86_64_v3-gnu": { @@ -25355,8 +25355,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "9a525d2a45db88cf7b450b2c0226204c1e5f634e3fb793f345ff6d44a9bac0fb", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.9.23%2B20250702-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "071b71c4a41da3cde092d877e36ce55f4906246c9d0755a3a349717ad4b1d7a5", "variant": "debug" }, "cpython-3.9.23+debug-linux-x86_64_v3-musl": { @@ -25371,8 +25371,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-x86_64_v3-unknown-linux-musl-debug-full.tar.zst", - "sha256": "7370898765b907757b7220db752c91de9a1832460d665845c74c4c946bbdd4ec", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.9.23%2B20250702-x86_64_v3-unknown-linux-musl-debug-full.tar.zst", + "sha256": "cd3c0e2060fe94dcd346add4ee9f9053bcc35367cd2b69b46c126f4ac0681aed", "variant": "debug" }, "cpython-3.9.23+debug-linux-x86_64_v4-gnu": { @@ -25387,8 +25387,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "c104d6d5d6943ecf53b9cc8560a32239a62c030a1170d1d4c686e87d7da60d58", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.9.23%2B20250702-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "3934b72131d7a00c5aeaec79c714315e6773bd4170596fb27265efb643444520", "variant": "debug" }, "cpython-3.9.23+debug-linux-x86_64_v4-musl": { @@ -25403,8 +25403,8 @@ "minor": 9, "patch": 23, "prerelease": "", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250630/cpython-3.9.23%2B20250630-x86_64_v4-unknown-linux-musl-debug-full.tar.zst", - "sha256": "933c85357368d1819a32bed32cdb871153ecc2a39b93565ae61e5fa0b509e2eb", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250702/cpython-3.9.23%2B20250702-x86_64_v4-unknown-linux-musl-debug-full.tar.zst", + "sha256": "dd0957b5c94d98f94a267e3d4e8e6acc3561f9b7970532d69d533b3eb59c72e6", "variant": "debug" }, "cpython-3.9.22-darwin-aarch64-none": { diff --git a/crates/uv-python/src/sysconfig/generated_mappings.rs b/crates/uv-python/src/sysconfig/generated_mappings.rs index 850b2c764..2611c1ac0 100644 --- a/crates/uv-python/src/sysconfig/generated_mappings.rs +++ b/crates/uv-python/src/sysconfig/generated_mappings.rs @@ -1,7 +1,7 @@ //! DO NOT EDIT //! //! Generated with `cargo run dev generate-sysconfig-metadata` -//! Targets from +//! Targets from //! #![allow(clippy::all)] #![cfg_attr(any(), rustfmt::skip)] @@ -15,7 +15,6 @@ use crate::sysconfig::replacements::{ReplacementEntry, ReplacementMode}; pub(crate) static DEFAULT_VARIABLE_UPDATES: LazyLock>> = LazyLock::new(|| { BTreeMap::from_iter([ ("BLDSHARED".to_string(), vec![ - ReplacementEntry { mode: ReplacementMode::Partial { from: "/usr/bin/aarch64-linux-gnu-gcc".to_string() }, to: "cc".to_string() }, ReplacementEntry { mode: ReplacementMode::Partial { from: "/usr/bin/arm-linux-gnueabi-gcc".to_string() }, to: "cc".to_string() }, ReplacementEntry { mode: ReplacementMode::Partial { from: "/usr/bin/arm-linux-gnueabihf-gcc".to_string() }, to: "cc".to_string() }, ReplacementEntry { mode: ReplacementMode::Partial { from: "/usr/bin/mips-linux-gnu-gcc".to_string() }, to: "cc".to_string() }, @@ -28,7 +27,6 @@ pub(crate) static DEFAULT_VARIABLE_UPDATES: LazyLock Date: Wed, 2 Jul 2025 15:11:50 -0500 Subject: [PATCH 166/185] Fix `workspace_unsatisfiable_member_dependencies` (#14429) --- crates/uv/tests/it/workspace.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/uv/tests/it/workspace.rs b/crates/uv/tests/it/workspace.rs index c52c4a2f1..631e4f6c3 100644 --- a/crates/uv/tests/it/workspace.rs +++ b/crates/uv/tests/it/workspace.rs @@ -1351,7 +1351,7 @@ fn workspace_unsatisfiable_member_dependencies() -> Result<()> { leaf.child("src/__init__.py").touch()?; // Resolving should fail. - uv_snapshot!(context.filters(), context.lock().current_dir(&workspace), @r###" + uv_snapshot!(context.filters(), context.lock().current_dir(&workspace), @r" success: false exit_code: 1 ----- stdout ----- @@ -1359,9 +1359,9 @@ fn workspace_unsatisfiable_member_dependencies() -> Result<()> { ----- stderr ----- Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] × No solution found when resolving dependencies: - ╰─▶ Because only httpx<=1.0.0b0 is available and leaf depends on httpx>9999, we can conclude that leaf's requirements are unsatisfiable. + ╰─▶ Because only httpx<=0.27.0 is available and leaf depends on httpx>9999, we can conclude that leaf's requirements are unsatisfiable. And because your workspace requires leaf, we can conclude that your workspace's requirements are unsatisfiable. - "### + " ); Ok(()) From 5f2857a1c71a8f9d7c18a8e697ae5bd93a448cdf Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Wed, 2 Jul 2025 15:18:46 -0500 Subject: [PATCH 167/185] Add linux aarch64 smoke tests (#14427) Testing https://github.com/astral-sh/uv/pull/14426 --- .github/workflows/ci.yml | 52 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 35ceb6875..d102f87cf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -470,6 +470,31 @@ jobs: ./target/debug/uvx retention-days: 1 + build-binary-linux-aarch64: + timeout-minutes: 10 + needs: determine_changes + if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }} + runs-on: github-ubuntu-24.04-aarch64-4 + name: "build binary | linux aarch64" + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - uses: rui314/setup-mold@v1 + + - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0 + + - name: "Build" + run: cargo build + + - name: "Upload binary" + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: uv-linux-aarch64-${{ github.sha }} + path: | + ./target/debug/uv + ./target/debug/uvx + retention-days: 1 + build-binary-linux-musl: timeout-minutes: 10 needs: determine_changes @@ -770,6 +795,33 @@ jobs: eval "$(./uv generate-shell-completion bash)" eval "$(./uvx --generate-shell-completion bash)" + smoke-test-linux-aarch64: + timeout-minutes: 10 + needs: build-binary-linux-aarch64 + name: "smoke test | linux aarch64" + runs-on: github-ubuntu-24.04-aarch64-2 + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: "Download binary" + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: uv-linux-aarch64-${{ github.sha }} + + - name: "Prepare binary" + run: | + chmod +x ./uv + chmod +x ./uvx + + - name: "Smoke test" + run: | + ./uv run scripts/smoke-test + + - name: "Test shell completions" + run: | + eval "$(./uv generate-shell-completion bash)" + eval "$(./uvx --generate-shell-completion bash)" + smoke-test-linux-musl: timeout-minutes: 10 needs: build-binary-linux-musl From 71b5ba13d76ad9e1b5bdf48941022fa7e89e4f3b Mon Sep 17 00:00:00 2001 From: konsti Date: Wed, 2 Jul 2025 22:37:43 +0200 Subject: [PATCH 168/185] Stabilize the uv build backend (#14311) The uv build backend has gone through some feedback cycles, we expect no more major configuration changes, and we're ready to take the next step: The uv build backend in stable. This PR stabilizes: * Using `uv_build` as build backend * The documentation of the uv build backend * The direct build fast path, where uv doesn't use PEP 517 if you're using `uv_build` in a compatible version. * `uv build --list`, which is limited to `uv_build`. It does not: * Make `uv_build` the default on `uv init` * Make `--package` the default on `uv init` --- crates/uv-build-backend/src/settings.rs | 4 ---- crates/uv-dispatch/src/lib.rs | 6 ------ crates/uv/src/commands/build_frontend.rs | 14 +------------- crates/uv/tests/it/build_backend.rs | 19 +++++-------------- docs/concepts/build-backend.md | 5 ++--- docs/reference/settings.md | 4 ---- uv.schema.json | 2 +- 7 files changed, 9 insertions(+), 45 deletions(-) diff --git a/crates/uv-build-backend/src/settings.rs b/crates/uv-build-backend/src/settings.rs index fc495c268..3b413e8e3 100644 --- a/crates/uv-build-backend/src/settings.rs +++ b/crates/uv-build-backend/src/settings.rs @@ -4,10 +4,6 @@ use uv_macros::OptionsMetadata; /// Settings for the uv build backend (`uv_build`). /// -/// !!! note -/// -/// The uv build backend is currently in preview and may change in any future release. -/// /// Note that those settings only apply when using the `uv_build` backend, other build backends /// (such as hatchling) have their own configuration. /// diff --git a/crates/uv-dispatch/src/lib.rs b/crates/uv-dispatch/src/lib.rs index 222450539..874e412e5 100644 --- a/crates/uv-dispatch/src/lib.rs +++ b/crates/uv-dispatch/src/lib.rs @@ -453,12 +453,6 @@ impl BuildContext for BuildDispatch<'_> { build_kind: BuildKind, version_id: Option<&'data str>, ) -> Result, BuildDispatchError> { - // Direct builds are a preview feature with the uv build backend. - if self.preview.is_disabled() { - trace!("Preview is disabled, not checking for direct build"); - return Ok(None); - } - let source_tree = if let Some(subdir) = subdirectory { source.join(subdir) } else { diff --git a/crates/uv/src/commands/build_frontend.rs b/crates/uv/src/commands/build_frontend.rs index bccb99fae..2cef9a406 100644 --- a/crates/uv/src/commands/build_frontend.rs +++ b/crates/uv/src/commands/build_frontend.rs @@ -187,15 +187,6 @@ async fn build_impl( printer: Printer, preview: PreviewMode, ) -> Result { - if list && preview.is_disabled() { - // We need the direct build for list and that is preview only. - writeln!( - printer.stderr(), - "The `--list` option is only available in preview mode; add the `--preview` flag to use `--list`" - )?; - return Ok(BuildResult::Failure); - } - // Extract the resolver settings. let ResolverSettings { index_locations, @@ -605,10 +596,7 @@ async fn build_package( } BuildAction::List - } else if preview.is_enabled() - && !force_pep517 - && check_direct_build(source.path(), source.path().user_display()) - { + } else if !force_pep517 && check_direct_build(source.path(), source.path().user_display()) { BuildAction::DirectBuild } else { BuildAction::Pep517 diff --git a/crates/uv/tests/it/build_backend.rs b/crates/uv/tests/it/build_backend.rs index 84b0ed8fe..b3bd337ae 100644 --- a/crates/uv/tests/it/build_backend.rs +++ b/crates/uv/tests/it/build_backend.rs @@ -224,7 +224,6 @@ fn preserve_executable_bit() -> Result<()> { .init() .arg("--build-backend") .arg("uv") - .arg("--preview") .arg(&project_dir) .assert() .success(); @@ -316,8 +315,7 @@ fn rename_module() -> Result<()> { uv_snapshot!(context .build_backend() .arg("build-wheel") - .arg(temp_dir.path()) - .env("UV_PREVIEW", "1"), @r###" + .arg(temp_dir.path()), @r###" success: true exit_code: 0 ----- stdout ----- @@ -391,8 +389,7 @@ fn rename_module_editable_build() -> Result<()> { uv_snapshot!(context .build_backend() .arg("build-editable") - .arg(temp_dir.path()) - .env("UV_PREVIEW", "1"), @r###" + .arg(temp_dir.path()), @r###" success: true exit_code: 0 ----- stdout ----- @@ -568,8 +565,7 @@ fn build_sdist_with_long_path() -> Result<()> { uv_snapshot!(context .build_backend() .arg("build-sdist") - .arg(temp_dir.path()) - .env("UV_PREVIEW", "1"), @r###" + .arg(temp_dir.path()), @r###" success: true exit_code: 0 ----- stdout ----- @@ -602,8 +598,7 @@ fn sdist_error_without_module() -> Result<()> { uv_snapshot!(context .build_backend() .arg("build-sdist") - .arg(temp_dir.path()) - .env("UV_PREVIEW", "1"), @r" + .arg(temp_dir.path()), @r" success: false exit_code: 2 ----- stdout ----- @@ -617,8 +612,7 @@ fn sdist_error_without_module() -> Result<()> { uv_snapshot!(context .build_backend() .arg("build-sdist") - .arg(temp_dir.path()) - .env("UV_PREVIEW", "1"), @r" + .arg(temp_dir.path()), @r" success: false exit_code: 2 ----- stdout ----- @@ -682,7 +676,6 @@ fn complex_namespace_packages() -> Result<()> { context .build() - .arg("--preview") .arg(project.path()) .arg("--out-dir") .arg(dist.path()) @@ -731,7 +724,6 @@ fn complex_namespace_packages() -> Result<()> { context.filters(), context .pip_install() - .arg("--preview") .arg("-e") .arg("complex-project-part_a") .arg("-e") @@ -778,7 +770,6 @@ fn symlinked_file() -> Result<()> { let project = context.temp_dir.child("project"); context .init() - .arg("--preview") .arg("--build-backend") .arg("uv") .arg(project.path()) diff --git a/docs/concepts/build-backend.md b/docs/concepts/build-backend.md index d70f00282..b4d276462 100644 --- a/docs/concepts/build-backend.md +++ b/docs/concepts/build-backend.md @@ -2,9 +2,8 @@ !!! note - The uv build backend is currently in preview and may change without warning. - - When preview mode is not enabled, uv uses [hatchling](https://pypi.org/project/hatchling/) as the default build backend. + Currently, the default build backend for `uv init` is + [hatchling](https://pypi.org/project/hatchling/). This will change to `uv` in a future version. A build backend transforms a source tree (i.e., a directory) into a source distribution or a wheel. diff --git a/docs/reference/settings.md b/docs/reference/settings.md index f681690f4..58948c80e 100644 --- a/docs/reference/settings.md +++ b/docs/reference/settings.md @@ -396,10 +396,6 @@ pydantic = { path = "/path/to/pydantic", editable = true } Settings for the uv build backend (`uv_build`). -!!! note - - The uv build backend is currently in preview and may change in any future release. - Note that those settings only apply when using the `uv_build` backend, other build backends (such as hatchling) have their own configuration. diff --git a/uv.schema.json b/uv.schema.json index 26d6ac7a3..dbc4f1168 100644 --- a/uv.schema.json +++ b/uv.schema.json @@ -644,7 +644,7 @@ ] }, "BuildBackendSettings": { - "description": "Settings for the uv build backend (`uv_build`).\n\n!!! note\n\n The uv build backend is currently in preview and may change in any future release.\n\nNote that those settings only apply when using the `uv_build` backend, other build backends\n(such as hatchling) have their own configuration.\n\nAll options that accept globs use the portable glob patterns from\n[PEP 639](https://packaging.python.org/en/latest/specifications/glob-patterns/).", + "description": "Settings for the uv build backend (`uv_build`).\n\nNote that those settings only apply when using the `uv_build` backend, other build backends\n(such as hatchling) have their own configuration.\n\nAll options that accept globs use the portable glob patterns from\n[PEP 639](https://packaging.python.org/en/latest/specifications/glob-patterns/).", "type": "object", "properties": { "data": { From 38ee6ec80096e69a244e82b9cdc3c12feceada7f Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Wed, 2 Jul 2025 16:19:52 -0500 Subject: [PATCH 169/185] Bump version to 0.7.19 (#14431) --- CHANGELOG.md | 39 +++++++++++++++++++++++++++ Cargo.lock | 6 ++--- crates/uv-build/Cargo.toml | 2 +- crates/uv-build/pyproject.toml | 2 +- crates/uv-version/Cargo.toml | 2 +- crates/uv/Cargo.toml | 2 +- docs/concepts/build-backend.md | 12 ++++----- docs/getting-started/installation.md | 4 +-- docs/guides/integration/aws-lambda.md | 4 +-- docs/guides/integration/docker.md | 10 +++---- docs/guides/integration/github.md | 2 +- docs/guides/integration/pre-commit.md | 10 +++---- pyproject.toml | 2 +- 13 files changed, 68 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2605248ba..c1c163331 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,43 @@ +## 0.7.19 + +The **[uv build backend](https://docs.astral.sh/uv/concepts/build-backend/) is now stable**, and considered ready for production use. + +The uv build backend is a great choice for pure Python projects. It has reasonable defaults, with the goal of requiring zero configuration for most users, but provides flexible configuration to accommodate most Python project structures. It integrates tightly with uv, to improve messaging and user experience. It validates project metadata and structures, preventing common mistakes. And, finally, it's very fast — `uv sync` on a new project (from `uv init`) is 10-30x faster than with other build backends. + +To use uv as a build backend in an existing project, add `uv_build` to the `[build-system]` section in your `pyproject.toml`: + +```toml +[build-system] +requires = ["uv_build>=0.7.19,<0.8.0"] +build-backend = "uv_build" +``` + +In a future release, it will replace `hatchling` as the default in `uv init`. As before, uv will remain compatible with all standards-compliant build backends. + +### Python + +- Add PGO distributions of Python for aarch64 Linux, which are more optimized for better performance + +See the [python-build-standalone release](https://github.com/astral-sh/python-build-standalone/releases/tag/20250702) for more details. + +### Enhancements + +- Ignore Python patch version for `--universal` pip compile ([#14405](https://github.com/astral-sh/uv/pull/14405)) +- Update the tilde version specifier warning to include more context ([#14335](https://github.com/astral-sh/uv/pull/14335)) +- Clarify behavior and hint on tool install when no executables are available ([#14423](https://github.com/astral-sh/uv/pull/14423)) + +### Bug fixes + +- Make project and interpreter lock acquisition non-fatal ([#14404](https://github.com/astral-sh/uv/pull/14404)) +- Includes `sys.prefix` in cached environment keys to avoid `--with` collisions across projects ([#14403](https://github.com/astral-sh/uv/pull/14403)) + +### Documentation + +- Add a migration guide from pip to uv projects ([#12382](https://github.com/astral-sh/uv/pull/12382)) + ## 0.7.18 ### Python @@ -12,6 +49,8 @@ These are not downloaded by default, since x86-64 Python has broader ecosystem support on Windows. However, they can be requested with `cpython--windows-aarch64`. +See the [python-build-standalone release](https://github.com/astral-sh/python-build-standalone/releases/tag/20250630) for more details. + ### Enhancements - Keep track of retries in `ManagedPythonDownload::fetch_with_retry` ([#14378](https://github.com/astral-sh/uv/pull/14378)) diff --git a/Cargo.lock b/Cargo.lock index 881cd8423..f9e51c47a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4603,7 +4603,7 @@ dependencies = [ [[package]] name = "uv" -version = "0.7.18" +version = "0.7.19" dependencies = [ "anstream", "anyhow", @@ -4767,7 +4767,7 @@ dependencies = [ [[package]] name = "uv-build" -version = "0.7.18" +version = "0.7.19" dependencies = [ "anyhow", "uv-build-backend", @@ -5957,7 +5957,7 @@ dependencies = [ [[package]] name = "uv-version" -version = "0.7.18" +version = "0.7.19" [[package]] name = "uv-virtualenv" diff --git a/crates/uv-build/Cargo.toml b/crates/uv-build/Cargo.toml index 83c37cd15..34dfa996a 100644 --- a/crates/uv-build/Cargo.toml +++ b/crates/uv-build/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-build" -version = "0.7.18" +version = "0.7.19" edition.workspace = true rust-version.workspace = true homepage.workspace = true diff --git a/crates/uv-build/pyproject.toml b/crates/uv-build/pyproject.toml index f48e0ddff..660e95c95 100644 --- a/crates/uv-build/pyproject.toml +++ b/crates/uv-build/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uv-build" -version = "0.7.18" +version = "0.7.19" description = "The uv build backend" authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }] requires-python = ">=3.8" diff --git a/crates/uv-version/Cargo.toml b/crates/uv-version/Cargo.toml index c17f17695..9b9ccd9bd 100644 --- a/crates/uv-version/Cargo.toml +++ b/crates/uv-version/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-version" -version = "0.7.18" +version = "0.7.19" edition = { workspace = true } rust-version = { workspace = true } homepage = { workspace = true } diff --git a/crates/uv/Cargo.toml b/crates/uv/Cargo.toml index 1b8d878ee..0a352d2b1 100644 --- a/crates/uv/Cargo.toml +++ b/crates/uv/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv" -version = "0.7.18" +version = "0.7.19" edition = { workspace = true } rust-version = { workspace = true } homepage = { workspace = true } diff --git a/docs/concepts/build-backend.md b/docs/concepts/build-backend.md index b4d276462..e68069ddb 100644 --- a/docs/concepts/build-backend.md +++ b/docs/concepts/build-backend.md @@ -13,11 +13,11 @@ performance and user experience. ## Choosing a build backend -The uv build backend is a good choice for most Python projects that are using uv. It has reasonable -defaults, with the goal of requiring zero configuration for most users, but provides flexible -configuration that allows most Python project structures. It integrates tightly with uv, to improve -messaging and user experience. It validates project metadata and structures, preventing common -mistakes. And, finally, it's very fast. +The uv build backend is a great choice for most Python projects. It has reasonable defaults, with +the goal of requiring zero configuration for most users, but provides flexible configuration to +accommodate most Python project structures. It integrates tightly with uv, to improve messaging and +user experience. It validates project metadata and structures, preventing common mistakes. And, +finally, it's very fast. The uv build backend currently **only supports pure Python code**. An alternative backend is required to build a @@ -36,7 +36,7 @@ To use uv as a build backend in an existing project, add `uv_build` to the ```toml title="pyproject.toml" [build-system] -requires = ["uv_build>=0.7.18,<0.8.0"] +requires = ["uv_build>=0.7.19,<0.8.0"] build-backend = "uv_build" ``` diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 227b4df43..a507a3ade 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -25,7 +25,7 @@ uv provides a standalone installer to download and install uv: Request a specific version by including it in the URL: ```console - $ curl -LsSf https://astral.sh/uv/0.7.18/install.sh | sh + $ curl -LsSf https://astral.sh/uv/0.7.19/install.sh | sh ``` === "Windows" @@ -41,7 +41,7 @@ uv provides a standalone installer to download and install uv: Request a specific version by including it in the URL: ```pwsh-session - PS> powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/0.7.18/install.ps1 | iex" + PS> powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/0.7.19/install.ps1 | iex" ``` !!! tip diff --git a/docs/guides/integration/aws-lambda.md b/docs/guides/integration/aws-lambda.md index db45f1016..233969420 100644 --- a/docs/guides/integration/aws-lambda.md +++ b/docs/guides/integration/aws-lambda.md @@ -92,7 +92,7 @@ the second stage, we'll copy this directory over to the final image, omitting th other unnecessary files. ```dockerfile title="Dockerfile" -FROM ghcr.io/astral-sh/uv:0.7.18 AS uv +FROM ghcr.io/astral-sh/uv:0.7.19 AS uv # First, bundle the dependencies into the task root. FROM public.ecr.aws/lambda/python:3.13 AS builder @@ -334,7 +334,7 @@ And confirm that opening http://127.0.0.1:8000/ in a web browser displays, "Hell Finally, we'll update the Dockerfile to include the local library in the deployment package: ```dockerfile title="Dockerfile" -FROM ghcr.io/astral-sh/uv:0.7.18 AS uv +FROM ghcr.io/astral-sh/uv:0.7.19 AS uv # First, bundle the dependencies into the task root. FROM public.ecr.aws/lambda/python:3.13 AS builder diff --git a/docs/guides/integration/docker.md b/docs/guides/integration/docker.md index 27416a0fa..bfbae2a7b 100644 --- a/docs/guides/integration/docker.md +++ b/docs/guides/integration/docker.md @@ -31,7 +31,7 @@ $ docker run --rm -it ghcr.io/astral-sh/uv:debian uv --help The following distroless images are available: - `ghcr.io/astral-sh/uv:latest` -- `ghcr.io/astral-sh/uv:{major}.{minor}.{patch}`, e.g., `ghcr.io/astral-sh/uv:0.7.18` +- `ghcr.io/astral-sh/uv:{major}.{minor}.{patch}`, e.g., `ghcr.io/astral-sh/uv:0.7.19` - `ghcr.io/astral-sh/uv:{major}.{minor}`, e.g., `ghcr.io/astral-sh/uv:0.7` (the latest patch version) @@ -75,7 +75,7 @@ And the following derived images are available: As with the distroless image, each derived image is published with uv version tags as `ghcr.io/astral-sh/uv:{major}.{minor}.{patch}-{base}` and -`ghcr.io/astral-sh/uv:{major}.{minor}-{base}`, e.g., `ghcr.io/astral-sh/uv:0.7.18-alpine`. +`ghcr.io/astral-sh/uv:{major}.{minor}-{base}`, e.g., `ghcr.io/astral-sh/uv:0.7.19-alpine`. For more details, see the [GitHub Container](https://github.com/astral-sh/uv/pkgs/container/uv) page. @@ -113,7 +113,7 @@ Note this requires `curl` to be available. In either case, it is best practice to pin to a specific uv version, e.g., with: ```dockerfile -COPY --from=ghcr.io/astral-sh/uv:0.7.18 /uv /uvx /bin/ +COPY --from=ghcr.io/astral-sh/uv:0.7.19 /uv /uvx /bin/ ``` !!! tip @@ -131,7 +131,7 @@ COPY --from=ghcr.io/astral-sh/uv:0.7.18 /uv /uvx /bin/ Or, with the installer: ```dockerfile -ADD https://astral.sh/uv/0.7.18/install.sh /uv-installer.sh +ADD https://astral.sh/uv/0.7.19/install.sh /uv-installer.sh ``` ### Installing a project @@ -557,5 +557,5 @@ Verified OK !!! tip These examples use `latest`, but best practice is to verify the attestation for a specific - version tag, e.g., `ghcr.io/astral-sh/uv:0.7.18`, or (even better) the specific image digest, + version tag, e.g., `ghcr.io/astral-sh/uv:0.7.19`, or (even better) the specific image digest, such as `ghcr.io/astral-sh/uv:0.5.27@sha256:5adf09a5a526f380237408032a9308000d14d5947eafa687ad6c6a2476787b4f`. diff --git a/docs/guides/integration/github.md b/docs/guides/integration/github.md index 60a18b8b4..de8853d05 100644 --- a/docs/guides/integration/github.md +++ b/docs/guides/integration/github.md @@ -47,7 +47,7 @@ jobs: uses: astral-sh/setup-uv@v5 with: # Install a specific version of uv. - version: "0.7.18" + version: "0.7.19" ``` ## Setting up Python diff --git a/docs/guides/integration/pre-commit.md b/docs/guides/integration/pre-commit.md index dbbc88462..b2f637a42 100644 --- a/docs/guides/integration/pre-commit.md +++ b/docs/guides/integration/pre-commit.md @@ -19,7 +19,7 @@ To make sure your `uv.lock` file is up to date even if your `pyproject.toml` fil repos: - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.7.18 + rev: 0.7.19 hooks: - id: uv-lock ``` @@ -30,7 +30,7 @@ To keep a `requirements.txt` file in sync with your `uv.lock` file: repos: - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.7.18 + rev: 0.7.19 hooks: - id: uv-export ``` @@ -41,7 +41,7 @@ To compile requirements files: repos: - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.7.18 + rev: 0.7.19 hooks: # Compile requirements - id: pip-compile @@ -54,7 +54,7 @@ To compile alternative requirements files, modify `args` and `files`: repos: - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.7.18 + rev: 0.7.19 hooks: # Compile requirements - id: pip-compile @@ -68,7 +68,7 @@ To run the hook over multiple files at the same time, add additional entries: repos: - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.7.18 + rev: 0.7.19 hooks: # Compile requirements - id: pip-compile diff --git a/pyproject.toml b/pyproject.toml index 7e73fa84c..5d4261360 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "uv" -version = "0.7.18" +version = "0.7.19" description = "An extremely fast Python package and project manager, written in Rust." authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }] requires-python = ">=3.8" From c3f13d2505f719dd8b145a42215dd62f72e67b8e Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Wed, 2 Jul 2025 20:02:17 -0500 Subject: [PATCH 170/185] Finish incomplete sentence in pip migration guide (#14432) Fixes https://github.com/astral-sh/uv/pull/12382#discussion_r2181237729 --- docs/guides/migration/pip-to-project.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/migration/pip-to-project.md b/docs/guides/migration/pip-to-project.md index a2be9ccd3..1356cb5d7 100644 --- a/docs/guides/migration/pip-to-project.md +++ b/docs/guides/migration/pip-to-project.md @@ -324,7 +324,7 @@ so multiple files are not needed to lock development dependencies. The uv lockfile is always [universal](../../concepts/resolution.md#universal-resolution), so multiple files are not needed to [lock dependencies for each platform](#platform-specific-dependencies). This ensures that all -developers +developers are using consistent, locked versions of dependencies regardless of their machine. The uv lockfile also supports concepts like [pinning packages to specific indexes](../../concepts/indexes.md#pinning-a-package-to-an-index), From 85c0fc963b1d4f6c8130c4b4446d15b8f6ac8ac4 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Thu, 3 Jul 2025 07:29:59 -0500 Subject: [PATCH 171/185] Fix forced resolution with all extras in `uv version` (#14434) Closes https://github.com/astral-sh/uv/issues/14433 Same as https://github.com/astral-sh/uv/pull/13380 --- crates/uv/src/commands/project/version.rs | 2 +- crates/uv/tests/it/version.rs | 54 +++++++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/crates/uv/src/commands/project/version.rs b/crates/uv/src/commands/project/version.rs index 102d91af6..bc79f8eb9 100644 --- a/crates/uv/src/commands/project/version.rs +++ b/crates/uv/src/commands/project/version.rs @@ -385,7 +385,7 @@ async fn lock_and_sync( let default_groups = default_dependency_groups(project.pyproject_toml())?; let default_extras = DefaultExtras::default(); let groups = DependencyGroups::default().with_defaults(default_groups); - let extras = ExtrasSpecification::from_all_extras().with_defaults(default_extras); + let extras = ExtrasSpecification::default().with_defaults(default_extras); let install_options = InstallOptions::default(); // Convert to an `AddTarget` by attaching the appropriate interpreter or environment. diff --git a/crates/uv/tests/it/version.rs b/crates/uv/tests/it/version.rs index 152cedaec..97d30f4f4 100644 --- a/crates/uv/tests/it/version.rs +++ b/crates/uv/tests/it/version.rs @@ -1958,3 +1958,57 @@ fn version_set_evil_constraints() -> Result<()> { Ok(()) } + +/// Bump the version with conflicting extras, to ensure we're activating the correct subset of +/// extras during the resolve. +#[test] +fn version_extras() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" +[project] +name = "myproject" +version = "1.10.31" +requires-python = ">=3.12" + +[project.optional-dependencies] +foo = ["requests"] +bar = ["httpx"] +baz = ["flask"] + +[tool.uv] +conflicts = [[{"extra" = "foo"}, {"extra" = "bar"}]] +"#, + )?; + + uv_snapshot!(context.filters(), context.version() + .arg("--bump").arg("patch"), @r" + success: true + exit_code: 0 + ----- stdout ----- + myproject 1.10.31 => 1.10.32 + + ----- stderr ----- + Resolved 19 packages in [TIME] + Audited in [TIME] + "); + + // Sync an extra, we should not remove it. + context.sync().arg("--extra").arg("foo").assert().success(); + + uv_snapshot!(context.filters(), context.version() + .arg("--bump").arg("patch"), @r" + success: true + exit_code: 0 + ----- stdout ----- + myproject 1.10.32 => 1.10.33 + + ----- stderr ----- + Resolved 19 packages in [TIME] + Audited in [TIME] + "); + + Ok(()) +} From 39cdfe9981655e6069e0072c0eacf2d4c877f622 Mon Sep 17 00:00:00 2001 From: konsti Date: Thu, 3 Jul 2025 15:34:44 +0200 Subject: [PATCH 172/185] Add a test for `--force-pep517` (#14310) There was previously a gap in the test coverage in ensuring that `--force-pep517` was respected. --- crates/uv/tests/it/build.rs | 65 ++++++++++++++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/crates/uv/tests/it/build.rs b/crates/uv/tests/it/build.rs index 706c1a681..0d4418d1a 100644 --- a/crates/uv/tests/it/build.rs +++ b/crates/uv/tests/it/build.rs @@ -133,7 +133,7 @@ fn build_sdist() -> Result<()> { let filters = context .filters() .into_iter() - .chain([(r"exit code: 1", "exit status: 1"), (r"\\\.", "")]) + .chain([(r"exit code: ", "exit status: "), (r"\\\.", "")]) .collect::>(); let project = context.temp_dir.child("project"); @@ -1981,3 +1981,66 @@ fn build_with_nonnormalized_name() -> Result<()> { Ok(()) } + +/// Check that `--force-pep517` is respected. +/// +/// The error messages for a broken project are different for direct builds vs. PEP 517. +#[test] +fn force_pep517() -> Result<()> { + // We need to use a real `uv_build` package. + let context = TestContext::new("3.12").with_exclude_newer("2025-05-27T00:00:00Z"); + + context + .init() + .arg("--build-backend") + .arg("uv") + .assert() + .success(); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! {r#" + [project] + name = "project" + version = "1.0.0" + + [tool.uv.build-backend] + module-name = "does_not_exist" + + [build-system] + requires = ["uv_build>=0.5.15,<10000"] + build-backend = "uv_build" + "#})?; + + uv_snapshot!(context.filters(), context.build().env("RUST_BACKTRACE", "0"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Building source distribution (uv build backend)... + × Failed to build `[TEMP_DIR]/` + ╰─▶ Expected a Python module at: `src/does_not_exist/__init__.py` + "); + + let filters = context + .filters() + .into_iter() + .chain([(r"exit code: 1", "exit status: 1")]) + .collect::>(); + + uv_snapshot!(filters, context.build().arg("--force-pep517").env("RUST_BACKTRACE", "0"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Building source distribution... + Error: Missing module directory for `does_not_exist` in `src`. Found: `temp` + × Failed to build `[TEMP_DIR]/` + ├─▶ The build backend returned an error + ╰─▶ Call to `uv_build.build_sdist` failed (exit status: 1) + hint: This usually indicates a problem with the package or the build environment. + "); + + Ok(()) +} From a1cda6213c6d4626cac3d6b45bfedc5ba3d2aeda Mon Sep 17 00:00:00 2001 From: konsti Date: Thu, 3 Jul 2025 15:50:40 +0200 Subject: [PATCH 173/185] Make "exit code" -> "exit status" a default filter (#14441) Remove some test boilerplate. Revival of https://github.com/astral-sh/uv/pull/14439 with main as base. --- crates/uv/tests/it/build.rs | 31 +++++++++++------------------- crates/uv/tests/it/common/mod.rs | 2 ++ crates/uv/tests/it/edit.rs | 10 ++-------- crates/uv/tests/it/lock.rs | 20 ++++--------------- crates/uv/tests/it/pip_compile.rs | 5 +---- crates/uv/tests/it/pip_install.rs | 30 ++++++----------------------- crates/uv/tests/it/sync.rs | 27 ++++++-------------------- crates/uv/tests/it/tool_install.rs | 1 - 8 files changed, 32 insertions(+), 94 deletions(-) diff --git a/crates/uv/tests/it/build.rs b/crates/uv/tests/it/build.rs index 0d4418d1a..3d08a90d4 100644 --- a/crates/uv/tests/it/build.rs +++ b/crates/uv/tests/it/build.rs @@ -15,7 +15,7 @@ fn build_basic() -> Result<()> { let filters = context .filters() .into_iter() - .chain([(r"exit code: 1", "exit status: 1"), (r"\\\.", "")]) + .chain([(r"\\\.", "")]) .collect::>(); let project = context.temp_dir.child("project"); @@ -133,7 +133,7 @@ fn build_sdist() -> Result<()> { let filters = context .filters() .into_iter() - .chain([(r"exit code: ", "exit status: "), (r"\\\.", "")]) + .chain([(r"\\\.", "")]) .collect::>(); let project = context.temp_dir.child("project"); @@ -189,7 +189,7 @@ fn build_wheel() -> Result<()> { let filters = context .filters() .into_iter() - .chain([(r"exit code: 1", "exit status: 1"), (r"\\\.", "")]) + .chain([(r"\\\.", "")]) .collect::>(); let project = context.temp_dir.child("project"); @@ -245,7 +245,7 @@ fn build_sdist_wheel() -> Result<()> { let filters = context .filters() .into_iter() - .chain([(r"exit code: 1", "exit status: 1"), (r"\\\.", "")]) + .chain([(r"\\\.", "")]) .collect::>(); let project = context.temp_dir.child("project"); @@ -303,7 +303,7 @@ fn build_wheel_from_sdist() -> Result<()> { let filters = context .filters() .into_iter() - .chain([(r"exit code: 1", "exit status: 1"), (r"\\\.", "")]) + .chain([(r"\\\.", "")]) .collect::>(); let project = context.temp_dir.child("project"); @@ -412,7 +412,7 @@ fn build_fail() -> Result<()> { let filters = context .filters() .into_iter() - .chain([(r"exit code: 1", "exit status: 1"), (r"\\\.", "")]) + .chain([(r"\\\.", "")]) .collect::>(); let project = context.temp_dir.child("project"); @@ -488,7 +488,6 @@ fn build_workspace() -> Result<()> { .filters() .into_iter() .chain([ - (r"exit code: 1", "exit status: 1"), (r"\\\.", ""), (r"\[project\]", "[PKG]"), (r"\[member\]", "[PKG]"), @@ -694,7 +693,6 @@ fn build_all_with_failure() -> Result<()> { .filters() .into_iter() .chain([ - (r"exit code: 1", "exit status: 1"), (r"\\\.", ""), (r"\[project\]", "[PKG]"), (r"\[member-\w+\]", "[PKG]"), @@ -840,7 +838,7 @@ fn build_constraints() -> Result<()> { let filters = context .filters() .into_iter() - .chain([(r"exit code: 1", "exit status: 1"), (r"\\\.", "")]) + .chain([(r"\\\.", "")]) .collect::>(); let project = context.temp_dir.child("project"); @@ -901,7 +899,7 @@ fn build_sha() -> Result<()> { let filters = context .filters() .into_iter() - .chain([(r"exit code: 1", "exit status: 1"), (r"\\\.", "")]) + .chain([(r"\\\.", "")]) .collect::>(); let project = context.temp_dir.child("project"); @@ -1187,7 +1185,7 @@ fn build_tool_uv_sources() -> Result<()> { let filters = context .filters() .into_iter() - .chain([(r"exit code: 1", "exit status: 1"), (r"\\\.", "")]) + .chain([(r"\\\.", "")]) .collect::>(); let build = context.temp_dir.child("backend"); @@ -1337,7 +1335,6 @@ fn build_non_package() -> Result<()> { .filters() .into_iter() .chain([ - (r"exit code: 1", "exit status: 1"), (r"\\\.", ""), (r"\[project\]", "[PKG]"), (r"\[member\]", "[PKG]"), @@ -1930,7 +1927,7 @@ fn build_with_nonnormalized_name() -> Result<()> { let filters = context .filters() .into_iter() - .chain([(r"exit code: 1", "exit status: 1"), (r"\\\.", "")]) + .chain([(r"\\\.", "")]) .collect::>(); let project = context.temp_dir.child("project"); @@ -2022,13 +2019,7 @@ fn force_pep517() -> Result<()> { ╰─▶ Expected a Python module at: `src/does_not_exist/__init__.py` "); - let filters = context - .filters() - .into_iter() - .chain([(r"exit code: 1", "exit status: 1")]) - .collect::>(); - - uv_snapshot!(filters, context.build().arg("--force-pep517").env("RUST_BACKTRACE", "0"), @r" + uv_snapshot!(context.filters(), context.build().arg("--force-pep517").env("RUST_BACKTRACE", "0"), @r" success: false exit_code: 2 ----- stdout ----- diff --git a/crates/uv/tests/it/common/mod.rs b/crates/uv/tests/it/common/mod.rs index 4c411899c..7b13c49b5 100644 --- a/crates/uv/tests/it/common/mod.rs +++ b/crates/uv/tests/it/common/mod.rs @@ -517,6 +517,8 @@ impl TestContext { if cfg!(windows) { filters.push((" --link-mode ".to_string(), String::new())); filters.push((r#"link-mode = "copy"\n"#.to_string(), String::new())); + // Unix uses "exit status", Windows uses "exit code" + filters.push((r"exit code: ".to_string(), "exit status: ".to_string())); } filters.extend( diff --git a/crates/uv/tests/it/edit.rs b/crates/uv/tests/it/edit.rs index ee0bf04ee..0ae2a07a6 100644 --- a/crates/uv/tests/it/edit.rs +++ b/crates/uv/tests/it/edit.rs @@ -7246,10 +7246,7 @@ fn fail_to_add_revert_project() -> Result<()> { .child("setup.py") .write_str("1/0")?; - let filters = std::iter::once((r"exit code: 1", "exit status: 1")) - .chain(context.filters()) - .collect::>(); - uv_snapshot!(filters, context.add().arg("./child"), @r#" + uv_snapshot!(context.filters(), context.add().arg("./child"), @r#" success: false exit_code: 1 ----- stdout ----- @@ -7351,10 +7348,7 @@ fn fail_to_edit_revert_project() -> Result<()> { .child("setup.py") .write_str("1/0")?; - let filters = std::iter::once((r"exit code: 1", "exit status: 1")) - .chain(context.filters()) - .collect::>(); - uv_snapshot!(filters, context.add().arg("./child"), @r#" + uv_snapshot!(context.filters(), context.add().arg("./child"), @r#" success: false exit_code: 1 ----- stdout ----- diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index 82754381b..5851022b8 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -23617,10 +23617,7 @@ fn lock_derivation_chain_prod() -> Result<()> { let filters = context .filters() .into_iter() - .chain([ - (r"exit code: 1", "exit status: 1"), - (r"/.*/src", "/[TMP]/src"), - ]) + .chain([(r"/.*/src", "/[TMP]/src")]) .collect::>(); uv_snapshot!(filters, context.lock(), @r###" @@ -23677,10 +23674,7 @@ fn lock_derivation_chain_extra() -> Result<()> { let filters = context .filters() .into_iter() - .chain([ - (r"exit code: 1", "exit status: 1"), - (r"/.*/src", "/[TMP]/src"), - ]) + .chain([(r"/.*/src", "/[TMP]/src")]) .collect::>(); uv_snapshot!(filters, context.lock(), @r###" @@ -23739,10 +23733,7 @@ fn lock_derivation_chain_group() -> Result<()> { let filters = context .filters() .into_iter() - .chain([ - (r"exit code: 1", "exit status: 1"), - (r"/.*/src", "/[TMP]/src"), - ]) + .chain([(r"/.*/src", "/[TMP]/src")]) .collect::>(); uv_snapshot!(filters, context.lock(), @r###" @@ -23812,10 +23803,7 @@ fn lock_derivation_chain_extended() -> Result<()> { let filters = context .filters() .into_iter() - .chain([ - (r"exit code: 1", "exit status: 1"), - (r"/.*/src", "/[TMP]/src"), - ]) + .chain([(r"/.*/src", "/[TMP]/src")]) .collect::>(); uv_snapshot!(filters, context.lock(), @r###" diff --git a/crates/uv/tests/it/pip_compile.rs b/crates/uv/tests/it/pip_compile.rs index ff135d959..b99be1296 100644 --- a/crates/uv/tests/it/pip_compile.rs +++ b/crates/uv/tests/it/pip_compile.rs @@ -14679,10 +14679,7 @@ fn compile_derivation_chain() -> Result<()> { let filters = context .filters() .into_iter() - .chain([ - (r"exit code: 1", "exit status: 1"), - (r"/.*/src", "/[TMP]/src"), - ]) + .chain([(r"/.*/src", "/[TMP]/src")]) .collect::>(); uv_snapshot!(filters, context.pip_compile().arg("pyproject.toml"), @r###" diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index 76b108a81..a33e08d90 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -342,10 +342,7 @@ dependencies = ["flask==1.0.x"] let requirements_txt = context.temp_dir.child("requirements.txt"); requirements_txt.write_str("./path_dep")?; - let filters = std::iter::once((r"exit code: 1", "exit status: 1")) - .chain(context.filters()) - .collect::>(); - uv_snapshot!(filters, context.pip_install() + uv_snapshot!(context.filters(), context.pip_install() .arg("-r") .arg("requirements.txt"), @r###" success: false @@ -4930,10 +4927,7 @@ fn no_build_isolation() -> Result<()> { requirements_in.write_str("anyio @ https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz")?; // We expect the build to fail, because `setuptools` is not installed. - let filters = std::iter::once((r"exit code: 1", "exit status: 1")) - .chain(context.filters()) - .collect::>(); - uv_snapshot!(filters, context.pip_install() + uv_snapshot!(context.filters(), context.pip_install() .arg("-r") .arg("requirements.in") .arg("--no-build-isolation"), @r###" @@ -5001,10 +4995,7 @@ fn respect_no_build_isolation_env_var() -> Result<()> { requirements_in.write_str("anyio @ https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz")?; // We expect the build to fail, because `setuptools` is not installed. - let filters = std::iter::once((r"exit code: 1", "exit status: 1")) - .chain(context.filters()) - .collect::>(); - uv_snapshot!(filters, context.pip_install() + uv_snapshot!(context.filters(), context.pip_install() .arg("-r") .arg("requirements.in") .env(EnvVars::UV_NO_BUILD_ISOLATION, "yes"), @r###" @@ -8601,10 +8592,7 @@ fn install_build_isolation_package() -> Result<()> { )?; // Running `uv pip install` should fail for iniconfig. - let filters = std::iter::once((r"exit code: 1", "exit status: 1")) - .chain(context.filters()) - .collect::>(); - uv_snapshot!(filters, context.pip_install() + uv_snapshot!(context.filters(), context.pip_install() .arg("--no-build-isolation-package") .arg("iniconfig") .arg(package.path()), @r###" @@ -8931,10 +8919,7 @@ fn missing_top_level() { fn sklearn() { let context = TestContext::new("3.12"); - let filters = std::iter::once((r"exit code: 1", "exit status: 1")) - .chain(context.filters()) - .collect::>(); - uv_snapshot!(filters, context.pip_install().arg("sklearn"), @r###" + uv_snapshot!(context.filters(), context.pip_install().arg("sklearn"), @r###" success: false exit_code: 1 ----- stdout ----- @@ -8984,10 +8969,7 @@ fn resolve_derivation_chain() -> Result<()> { let filters = context .filters() .into_iter() - .chain([ - (r"exit code: 1", "exit status: 1"), - (r"/.*/src", "/[TMP]/src"), - ]) + .chain([(r"/.*/src", "/[TMP]/src")]) .collect::>(); uv_snapshot!(filters, context.pip_install() diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index a97775d9f..f9a71fe82 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -1122,10 +1122,7 @@ fn sync_build_isolation_package() -> Result<()> { )?; // Running `uv sync` should fail for iniconfig. - let filters = std::iter::once((r"exit code: 1", "exit status: 1")) - .chain(context.filters()) - .collect::>(); - uv_snapshot!(filters, context.sync().arg("--no-build-isolation-package").arg("source-distribution"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--no-build-isolation-package").arg("source-distribution"), @r###" success: false exit_code: 1 ----- stdout ----- @@ -1215,10 +1212,7 @@ fn sync_build_isolation_extra() -> Result<()> { )?; // Running `uv sync` should fail for the `compile` extra. - let filters = std::iter::once((r"exit code: 1", "exit status: 1")) - .chain(context.filters()) - .collect::>(); - uv_snapshot!(&filters, context.sync().arg("--extra").arg("compile"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--extra").arg("compile"), @r###" success: false exit_code: 1 ----- stdout ----- @@ -1239,7 +1233,7 @@ fn sync_build_isolation_extra() -> Result<()> { "###); // Running `uv sync` with `--all-extras` should also fail. - uv_snapshot!(&filters, context.sync().arg("--all-extras"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--all-extras"), @r###" success: false exit_code: 1 ----- stdout ----- @@ -6985,10 +6979,7 @@ fn sync_derivation_chain() -> Result<()> { let filters = context .filters() .into_iter() - .chain([ - (r"exit code: 1", "exit status: 1"), - (r"/.*/src", "/[TMP]/src"), - ]) + .chain([(r"/.*/src", "/[TMP]/src")]) .collect::>(); uv_snapshot!(filters, context.sync(), @r###" @@ -7051,10 +7042,7 @@ fn sync_derivation_chain_extra() -> Result<()> { let filters = context .filters() .into_iter() - .chain([ - (r"exit code: 1", "exit status: 1"), - (r"/.*/src", "/[TMP]/src"), - ]) + .chain([(r"/.*/src", "/[TMP]/src")]) .collect::>(); uv_snapshot!(filters, context.sync().arg("--extra").arg("wsgi"), @r###" @@ -7119,10 +7107,7 @@ fn sync_derivation_chain_group() -> Result<()> { let filters = context .filters() .into_iter() - .chain([ - (r"exit code: 1", "exit status: 1"), - (r"/.*/src", "/[TMP]/src"), - ]) + .chain([(r"/.*/src", "/[TMP]/src")]) .collect::>(); uv_snapshot!(filters, context.sync().arg("--group").arg("wsgi"), @r###" diff --git a/crates/uv/tests/it/tool_install.rs b/crates/uv/tests/it/tool_install.rs index deb935f9b..6a2d38db8 100644 --- a/crates/uv/tests/it/tool_install.rs +++ b/crates/uv/tests/it/tool_install.rs @@ -1682,7 +1682,6 @@ fn tool_install_uninstallable() { .filters() .into_iter() .chain([ - (r"exit code: 1", "exit status: 1"), (r"bdist\.[^/\\\s]+(-[^/\\\s]+)?", "bdist.linux-x86_64"), (r"\\\.", ""), (r"#+", "#"), From 8afbd86f03b21cd8a77663da166b66aa0be62acf Mon Sep 17 00:00:00 2001 From: Simon Sure Date: Thu, 3 Jul 2025 17:43:59 +0100 Subject: [PATCH 174/185] make `ErrorTree` for `NoSolutionError` externally accessible (#14444) Hey, are you okay with exposing the `ErrorTree` for library consumers? We have a use case that needs more information on conflicts. We need the tree-structure of the conflict and be able to traverse it in particular. Signed-off-by: Simon Sure --- crates/uv-resolver/src/error.rs | 7 ++++++- crates/uv-resolver/src/lib.rs | 4 ++-- crates/uv-resolver/src/pubgrub/mod.rs | 2 +- crates/uv-resolver/src/pubgrub/package.rs | 6 +++--- crates/uv-resolver/src/resolver/availability.rs | 6 +++--- 5 files changed, 15 insertions(+), 10 deletions(-) diff --git a/crates/uv-resolver/src/error.rs b/crates/uv-resolver/src/error.rs index e327e8562..72f9217e3 100644 --- a/crates/uv-resolver/src/error.rs +++ b/crates/uv-resolver/src/error.rs @@ -156,7 +156,7 @@ impl From> for ResolveError { } } -pub(crate) type ErrorTree = DerivationTree, UnavailableReason>; +pub type ErrorTree = DerivationTree, UnavailableReason>; /// A wrapper around [`pubgrub::error::NoSolutionError`] that displays a resolution failure report. pub struct NoSolutionError { @@ -367,6 +367,11 @@ impl NoSolutionError { NoSolutionHeader::new(self.env.clone()) } + /// Get the conflict derivation tree for external analysis + pub fn derivation_tree(&self) -> &ErrorTree { + &self.error + } + /// Hint at limiting the resolver environment if universal resolution failed for a target /// that is not the current platform or not the current Python version. fn hint_disjoint_targets(&self, f: &mut Formatter) -> std::fmt::Result { diff --git a/crates/uv-resolver/src/lib.rs b/crates/uv-resolver/src/lib.rs index 48904660d..e91df3a7e 100644 --- a/crates/uv-resolver/src/lib.rs +++ b/crates/uv-resolver/src/lib.rs @@ -1,5 +1,5 @@ pub use dependency_mode::DependencyMode; -pub use error::{NoSolutionError, NoSolutionHeader, ResolveError, SentinelRange}; +pub use error::{ErrorTree, NoSolutionError, NoSolutionHeader, ResolveError, SentinelRange}; pub use exclude_newer::ExcludeNewer; pub use exclusions::Exclusions; pub use flat_index::{FlatDistributions, FlatIndex}; @@ -54,7 +54,7 @@ mod options; mod pins; mod preferences; mod prerelease; -mod pubgrub; +pub mod pubgrub; mod python_requirement; mod redirect; mod resolution; diff --git a/crates/uv-resolver/src/pubgrub/mod.rs b/crates/uv-resolver/src/pubgrub/mod.rs index f4802a2ca..bd58fbc72 100644 --- a/crates/uv-resolver/src/pubgrub/mod.rs +++ b/crates/uv-resolver/src/pubgrub/mod.rs @@ -1,6 +1,6 @@ pub(crate) use crate::pubgrub::dependencies::PubGrubDependency; pub(crate) use crate::pubgrub::distribution::PubGrubDistribution; -pub(crate) use crate::pubgrub::package::{PubGrubPackage, PubGrubPackageInner, PubGrubPython}; +pub use crate::pubgrub::package::{PubGrubPackage, PubGrubPackageInner, PubGrubPython}; pub(crate) use crate::pubgrub::priority::{PubGrubPriorities, PubGrubPriority, PubGrubTiebreaker}; pub(crate) use crate::pubgrub::report::PubGrubReportFormatter; diff --git a/crates/uv-resolver/src/pubgrub/package.rs b/crates/uv-resolver/src/pubgrub/package.rs index 8c40f8080..2e67a715a 100644 --- a/crates/uv-resolver/src/pubgrub/package.rs +++ b/crates/uv-resolver/src/pubgrub/package.rs @@ -9,7 +9,7 @@ use crate::python_requirement::PythonRequirement; /// [`Arc`] wrapper around [`PubGrubPackageInner`] to make cloning (inside PubGrub) cheap. #[derive(Debug, Clone, Eq, Hash, PartialEq, PartialOrd, Ord)] -pub(crate) struct PubGrubPackage(Arc); +pub struct PubGrubPackage(Arc); impl Deref for PubGrubPackage { type Target = PubGrubPackageInner; @@ -39,7 +39,7 @@ impl From for PubGrubPackage { /// package (e.g., `black[colorama]`), and mark it as a dependency of the real package (e.g., /// `black`). We then discard the virtual packages at the end of the resolution process. #[derive(Debug, Clone, Eq, Hash, PartialEq, PartialOrd, Ord)] -pub(crate) enum PubGrubPackageInner { +pub enum PubGrubPackageInner { /// The root package, which is used to start the resolution process. Root(Option), /// A Python version. @@ -295,7 +295,7 @@ impl PubGrubPackage { } #[derive(Debug, Copy, Clone, Eq, PartialEq, PartialOrd, Hash, Ord)] -pub(crate) enum PubGrubPython { +pub enum PubGrubPython { /// The Python version installed in the current environment. Installed, /// The Python version for which dependencies are being resolved. diff --git a/crates/uv-resolver/src/resolver/availability.rs b/crates/uv-resolver/src/resolver/availability.rs index d2e9296b9..64721b4b6 100644 --- a/crates/uv-resolver/src/resolver/availability.rs +++ b/crates/uv-resolver/src/resolver/availability.rs @@ -7,7 +7,7 @@ use uv_platform_tags::{AbiTag, Tags}; /// The reason why a package or a version cannot be used. #[derive(Debug, Clone, Eq, PartialEq)] -pub(crate) enum UnavailableReason { +pub enum UnavailableReason { /// The entire package cannot be used. Package(UnavailablePackage), /// A single version cannot be used. @@ -29,7 +29,7 @@ impl Display for UnavailableReason { /// Most variant are from [`MetadataResponse`] without the error source, since we don't format /// the source and we want to merge unavailable messages across versions. #[derive(Debug, Clone, Eq, PartialEq)] -pub(crate) enum UnavailableVersion { +pub enum UnavailableVersion { /// Version is incompatible because it has no usable distributions IncompatibleDist(IncompatibleDist), /// The wheel metadata was found, but could not be parsed. @@ -123,7 +123,7 @@ impl From<&MetadataUnavailable> for UnavailableVersion { /// The package is unavailable and cannot be used. #[derive(Debug, Clone, Eq, PartialEq)] -pub(crate) enum UnavailablePackage { +pub enum UnavailablePackage { /// Index lookups were disabled (i.e., `--no-index`) and the package was not found in a flat index (i.e. from `--find-links`). NoIndex, /// Network requests were disabled (i.e., `--offline`), and the package was not found in the cache. From 06af93fce761d3b9634185b6a94efa6c75ca8f8a Mon Sep 17 00:00:00 2001 From: konsti Date: Thu, 3 Jul 2025 22:29:03 +0200 Subject: [PATCH 175/185] Fix optional cfg gates (#14448) Running `cargo clippy` in individual crates could raise warnings due to unused imports as `Cow` is only used with `#[cfg(feature = "schemars")]` --- crates/uv-configuration/src/name_specifiers.rs | 4 +++- crates/uv-configuration/src/required_version.rs | 5 +++-- crates/uv-configuration/src/trusted_host.rs | 4 +++- crates/uv-distribution-types/src/pip_index.rs | 4 +++- crates/uv-distribution-types/src/status_code_strategy.rs | 4 +++- crates/uv-python/src/python_version.rs | 1 + crates/uv-resolver/src/exclude_newer.rs | 4 +++- crates/uv-workspace/src/pyproject.rs | 1 + 8 files changed, 20 insertions(+), 7 deletions(-) diff --git a/crates/uv-configuration/src/name_specifiers.rs b/crates/uv-configuration/src/name_specifiers.rs index 1fd25ea0b..3efeee1f2 100644 --- a/crates/uv-configuration/src/name_specifiers.rs +++ b/crates/uv-configuration/src/name_specifiers.rs @@ -1,4 +1,6 @@ -use std::{borrow::Cow, str::FromStr}; +#[cfg(feature = "schemars")] +use std::borrow::Cow; +use std::str::FromStr; use uv_pep508::PackageName; diff --git a/crates/uv-configuration/src/required_version.rs b/crates/uv-configuration/src/required_version.rs index 7135dfdab..70c69eaf3 100644 --- a/crates/uv-configuration/src/required_version.rs +++ b/crates/uv-configuration/src/required_version.rs @@ -1,5 +1,6 @@ -use std::str::FromStr; -use std::{borrow::Cow, fmt::Formatter}; +#[cfg(feature = "schemars")] +use std::borrow::Cow; +use std::{fmt::Formatter, str::FromStr}; use uv_pep440::{Version, VersionSpecifier, VersionSpecifiers, VersionSpecifiersParseError}; diff --git a/crates/uv-configuration/src/trusted_host.rs b/crates/uv-configuration/src/trusted_host.rs index 9f3efb6fc..07ff2998a 100644 --- a/crates/uv-configuration/src/trusted_host.rs +++ b/crates/uv-configuration/src/trusted_host.rs @@ -1,5 +1,7 @@ use serde::{Deserialize, Deserializer}; -use std::{borrow::Cow, str::FromStr}; +#[cfg(feature = "schemars")] +use std::borrow::Cow; +use std::str::FromStr; use url::Url; /// A host specification (wildcard, or host, with optional scheme and/or port) for which diff --git a/crates/uv-distribution-types/src/pip_index.rs b/crates/uv-distribution-types/src/pip_index.rs index 888c0df0d..18671e42f 100644 --- a/crates/uv-distribution-types/src/pip_index.rs +++ b/crates/uv-distribution-types/src/pip_index.rs @@ -3,7 +3,9 @@ //! flags set. use serde::{Deserialize, Deserializer, Serialize}; -use std::{borrow::Cow, path::Path}; +#[cfg(feature = "schemars")] +use std::borrow::Cow; +use std::path::Path; use crate::{Index, IndexUrl}; diff --git a/crates/uv-distribution-types/src/status_code_strategy.rs b/crates/uv-distribution-types/src/status_code_strategy.rs index 709f68be1..b019d0329 100644 --- a/crates/uv-distribution-types/src/status_code_strategy.rs +++ b/crates/uv-distribution-types/src/status_code_strategy.rs @@ -1,4 +1,6 @@ -use std::{borrow::Cow, ops::Deref}; +#[cfg(feature = "schemars")] +use std::borrow::Cow; +use std::ops::Deref; use http::StatusCode; use rustc_hash::FxHashSet; diff --git a/crates/uv-python/src/python_version.rs b/crates/uv-python/src/python_version.rs index 63f50f226..c5d8f6365 100644 --- a/crates/uv-python/src/python_version.rs +++ b/crates/uv-python/src/python_version.rs @@ -1,3 +1,4 @@ +#[cfg(feature = "schemars")] use std::borrow::Cow; use std::fmt::{Display, Formatter}; use std::ops::Deref; diff --git a/crates/uv-resolver/src/exclude_newer.rs b/crates/uv-resolver/src/exclude_newer.rs index c1ac6adb8..65fa55cfe 100644 --- a/crates/uv-resolver/src/exclude_newer.rs +++ b/crates/uv-resolver/src/exclude_newer.rs @@ -1,4 +1,6 @@ -use std::{borrow::Cow, str::FromStr}; +#[cfg(feature = "schemars")] +use std::borrow::Cow; +use std::str::FromStr; use jiff::{Timestamp, ToSpan, tz::TimeZone}; diff --git a/crates/uv-workspace/src/pyproject.rs b/crates/uv-workspace/src/pyproject.rs index 41e20914f..124a62881 100644 --- a/crates/uv-workspace/src/pyproject.rs +++ b/crates/uv-workspace/src/pyproject.rs @@ -6,6 +6,7 @@ //! //! Then lowers them into a dependency specification. +#[cfg(feature = "schemars")] use std::borrow::Cow; use std::collections::BTreeMap; use std::fmt::Formatter; From e8bc3950ef9bb276b3b03d96e8d38cafb4052af2 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 3 Jul 2025 19:32:07 -0400 Subject: [PATCH 176/185] Remove transparent variants in `uv-extract` to enable retries (#14450) ## Summary We think this is the culprit for the lack of retries in some settings (e.g., Python downloads). See: https://github.com/astral-sh/uv/issues/14425. --- crates/uv-extract/src/error.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/uv-extract/src/error.rs b/crates/uv-extract/src/error.rs index 09191bb0a..ae2fdff1a 100644 --- a/crates/uv-extract/src/error.rs +++ b/crates/uv-extract/src/error.rs @@ -2,11 +2,11 @@ use std::{ffi::OsString, path::PathBuf}; #[derive(Debug, thiserror::Error)] pub enum Error { - #[error(transparent)] + #[error("Failed to read from zip file")] Zip(#[from] zip::result::ZipError), - #[error(transparent)] + #[error("Failed to read from zip file")] AsyncZip(#[from] async_zip::error::ZipError), - #[error(transparent)] + #[error("I/O operation failed during extraction")] Io(#[from] std::io::Error), #[error( "The top-level of the archive must only contain a list directory, but it contains: {0:?}" From eaf517efd85f3fcaafe290ffc36615f4be8e04f9 Mon Sep 17 00:00:00 2001 From: Tim de Jager Date: Fri, 4 Jul 2025 20:08:23 +0200 Subject: [PATCH 177/185] Add method to get packages involved in a `NoSolutionError` (#14457) ## Summary In pixi we overlay the PyPI packages over the conda packages and we sometimes need to figure out what PyPI packages are involved in the no-solution error. We could parse the error message, but this is pretty error-prone, so it would be good to get access to more information. A lot of information in this module is private and should probably stay this way, but package names are easy enough to expose. This would help us a lot! I collect into a HashSet to remove duplication, and did not want to expose a rustc_hash datastructure directly, thats's why I've chosen to expose as an iterator :) Let me know if any changes need to be done, and thanks! --------- Co-authored-by: Zanie Blue --- crates/uv-resolver/src/error.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/uv-resolver/src/error.rs b/crates/uv-resolver/src/error.rs index 72f9217e3..0916f54ac 100644 --- a/crates/uv-resolver/src/error.rs +++ b/crates/uv-resolver/src/error.rs @@ -3,6 +3,7 @@ use std::fmt::Formatter; use std::sync::Arc; use indexmap::IndexSet; +use itertools::Itertools; use owo_colors::OwoColorize; use pubgrub::{ DefaultStringReporter, DerivationTree, Derived, External, Range, Ranges, Reporter, Term, @@ -409,6 +410,15 @@ impl NoSolutionError { } Ok(()) } + + /// Get the packages that are involved in this error. + pub fn packages(&self) -> impl Iterator { + self.error + .packages() + .into_iter() + .filter_map(|p| p.name()) + .unique() + } } impl std::fmt::Debug for NoSolutionError { From f609e1ddaf0b9468cc362fcbf4a091bf8b46c7e3 Mon Sep 17 00:00:00 2001 From: John Mumm Date: Fri, 4 Jul 2025 22:42:56 +0200 Subject: [PATCH 178/185] Document that `VerbatimUrl` does not preserve original string after serialization (#14456) This came up in [discussion](https://github.com/astral-sh/uv/pull/14387#issuecomment-3032223670) on #14387. --- crates/uv-pep508/src/verbatim_url.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/uv-pep508/src/verbatim_url.rs b/crates/uv-pep508/src/verbatim_url.rs index 8b914eb74..c800ba10c 100644 --- a/crates/uv-pep508/src/verbatim_url.rs +++ b/crates/uv-pep508/src/verbatim_url.rs @@ -18,11 +18,16 @@ use uv_redacted::DisplaySafeUrl; use crate::Pep508Url; /// A wrapper around [`Url`] that preserves the original string. +/// +/// The original string is not preserved after serialization/deserialization. #[derive(Debug, Clone, Eq)] pub struct VerbatimUrl { /// The parsed URL. url: DisplaySafeUrl, /// The URL as it was provided by the user. + /// + /// Even if originally set, this will be [`None`] after + /// serialization/deserialization. given: Option, } From 1308c85efe856cf8c4d26125b5c999182adb2fc6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 7 Jul 2025 09:58:05 +0200 Subject: [PATCH 179/185] Update Rust crate async-channel to v2.5.0 (#14478) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [async-channel](https://redirect.github.com/smol-rs/async-channel) | workspace.dependencies | minor | `2.3.1` -> `2.5.0` | --- > [!WARNING] > Some dependencies could not be looked up. Check the Dependency Dashboard for more information. --- ### Release Notes
    smol-rs/async-channel (async-channel) ### [`v2.5.0`](https://redirect.github.com/smol-rs/async-channel/blob/HEAD/CHANGELOG.md#Version-250) [Compare Source](https://redirect.github.com/smol-rs/async-channel/compare/v2.4.0...v2.5.0) - Add `Sender::closed()` ([#​102](https://redirect.github.com/smol-rs/async-channel/issues/102)) ### [`v2.4.0`](https://redirect.github.com/smol-rs/async-channel/blob/HEAD/CHANGELOG.md#Version-240) [Compare Source](https://redirect.github.com/smol-rs/async-channel/compare/v2.3.1...v2.4.0) - Add `Sender::same_channel()` and `Receiver::same_channel()`. ([#​98](https://redirect.github.com/smol-rs/async-channel/issues/98)) - Add `portable-atomic` feature to support platforms without atomics. ([#​106](https://redirect.github.com/smol-rs/async-channel/issues/106))
    --- ### Configuration 📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/astral-sh/uv). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f9e51c47a..1e77b0505 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -189,9 +189,9 @@ dependencies = [ [[package]] name = "async-channel" -version = "2.3.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" dependencies = [ "concurrent-queue", "event-listener-strategy", @@ -1165,9 +1165,9 @@ dependencies = [ [[package]] name = "event-listener-strategy" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3e4e0dd3673c1139bf041f3008816d9cf2946bbfac2945c09e523b8d7b05b2" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" dependencies = [ "event-listener", "pin-project-lite", From fc758bb75538244ec26066e059da99e618c88ceb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 7 Jul 2025 10:00:52 +0200 Subject: [PATCH 180/185] Update Rust crate schemars to v1.0.4 (#14476) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [schemars](https://graham.cool/schemars/) ([source](https://redirect.github.com/GREsau/schemars)) | workspace.dependencies | patch | `1.0.3` -> `1.0.4` | --- > [!WARNING] > Some dependencies could not be looked up. Check the Dependency Dashboard for more information. --- ### Release Notes
    GREsau/schemars (schemars) ### [`v1.0.4`](https://redirect.github.com/GREsau/schemars/blob/HEAD/CHANGELOG.md#104---2025-07-06) [Compare Source](https://redirect.github.com/GREsau/schemars/compare/v1.0.3...v1.0.4) ##### Fixed - Fix `JsonSchema` impl on [atomic](https://doc.rust-lang.org/std/sync/atomic/) types being ignored on non-nightly compilers due to a buggy `cfg` check ([https://github.com/GREsau/schemars/issues/453](https://redirect.github.com/GREsau/schemars/issues/453)) - Fix compatibility with minimal dependency versions, e.g. old(-ish) versions of `syn` ([https://github.com/GREsau/schemars/issues/450](https://redirect.github.com/GREsau/schemars/issues/450)) - Fix derive for empty tuple variants ([https://github.com/GREsau/schemars/issues/455](https://redirect.github.com/GREsau/schemars/issues/455))
    --- ### Configuration 📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/astral-sh/uv). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1e77b0505..12d28a238 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3428,9 +3428,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1375ba8ef45a6f15d83fa8748f1079428295d403d6ea991d09ab100155fbc06d" +checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" dependencies = [ "dyn-clone", "ref-cast", @@ -3442,9 +3442,9 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b13ed22d6d49fe23712e068770b5c4df4a693a2b02eeff8e7ca3135627a24f6" +checksum = "33d020396d1d138dc19f1165df7545479dcd58d93810dc5d646a16e55abefa80" dependencies = [ "proc-macro2", "quote", From bb738aeb440d59f98a40284630002b15b3f3d797 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 7 Jul 2025 10:04:50 +0200 Subject: [PATCH 181/185] Update Rust crate test-log to v0.2.18 (#14477) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [test-log](https://redirect.github.com/d-e-s-o/test-log) | dev-dependencies | patch | `0.2.17` -> `0.2.18` | --- > [!WARNING] > Some dependencies could not be looked up. Check the Dependency Dashboard for more information. --- ### Release Notes
    d-e-s-o/test-log (test-log) ### [`v0.2.18`](https://redirect.github.com/d-e-s-o/test-log/blob/HEAD/CHANGELOG.md#0218) [Compare Source](https://redirect.github.com/d-e-s-o/test-log/compare/v0.2.17...v0.2.18) - Improved cooperation with other similar procedural macros to enable attribute stacking
    --- ### Configuration 📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/astral-sh/uv). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 12d28a238..ffadd8df2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3968,9 +3968,9 @@ dependencies = [ [[package]] name = "test-log" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7f46083d221181166e5b6f6b1e5f1d499f3a76888826e6cb1d057554157cd0f" +checksum = "1e33b98a582ea0be1168eba097538ee8dd4bbe0f2b01b22ac92ea30054e5be7b" dependencies = [ "test-log-macros", "tracing-subscriber", @@ -3978,9 +3978,9 @@ dependencies = [ [[package]] name = "test-log-macros" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "888d0c3c6db53c0fdab160d2ed5e12ba745383d3e85813f2ea0f2b1475ab553f" +checksum = "451b374529930d7601b1eef8d32bc79ae870b6079b069401709c2a8bf9e75f36" dependencies = [ "proc-macro2", "quote", From 1d027bd92ae35a5eded15c8bc1109023ad4b482a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 7 Jul 2025 10:36:26 +0200 Subject: [PATCH 182/185] Update pre-commit dependencies (#14474) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [astral-sh/ruff-pre-commit](https://redirect.github.com/astral-sh/ruff-pre-commit) | repository | patch | `v0.12.1` -> `v0.12.2` | | [crate-ci/typos](https://redirect.github.com/crate-ci/typos) | repository | minor | `v1.33.1` -> `v1.34.0` | --- > [!WARNING] > Some dependencies could not be looked up. Check the Dependency Dashboard for more information. Note: The `pre-commit` manager in Renovate is not supported by the `pre-commit` maintainers or community. Please do not report any problems there, instead [create a Discussion in the Renovate repository](https://redirect.github.com/renovatebot/renovate/discussions/new) if you have any questions. --- ### Release Notes
    astral-sh/ruff-pre-commit (astral-sh/ruff-pre-commit) ### [`v0.12.2`](https://redirect.github.com/astral-sh/ruff-pre-commit/releases/tag/v0.12.2) [Compare Source](https://redirect.github.com/astral-sh/ruff-pre-commit/compare/v0.12.1...v0.12.2) See: https://github.com/astral-sh/ruff/releases/tag/0.12.2
    crate-ci/typos (crate-ci/typos) ### [`v1.34.0`](https://redirect.github.com/crate-ci/typos/releases/tag/v1.34.0) [Compare Source](https://redirect.github.com/crate-ci/typos/compare/v1.33.1...v1.34.0) #### \[1.34.0] - 2025-06-30 ##### Features - Updated the dictionary with the [June 2025](https://redirect.github.com/crate-ci/typos/issues/1309) changes
    --- ### Configuration 📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://redirect.github.com/renovatebot/renovate/discussions) if that's undesired. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/astral-sh/uv). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2280f337b..1c8965c0f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,7 +12,7 @@ repos: - id: validate-pyproject - repo: https://github.com/crate-ci/typos - rev: v1.33.1 + rev: v1.34.0 hooks: - id: typos @@ -42,7 +42,7 @@ repos: types_or: [yaml, json5] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.1 + rev: v0.12.2 hooks: - id: ruff-format - id: ruff From 3a77b9cdd997777a6e464bcc60a0995ecf31b23b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 7 Jul 2025 11:31:31 +0200 Subject: [PATCH 183/185] Update aws-actions/configure-aws-credentials digest to f503a18 (#14473) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | aws-actions/configure-aws-credentials | action | digest | `3d8cba3` -> `f503a18` | --- > [!WARNING] > Some dependencies could not be looked up. Check the Dependency Dashboard for more information. --- ### Configuration 📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/astral-sh/uv). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d102f87cf..bc77abd93 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1585,7 +1585,7 @@ jobs: run: chmod +x ./uv - name: "Configure AWS credentials" - uses: aws-actions/configure-aws-credentials@3d8cba388a057b13744d61818a337e40a119b1a7 + uses: aws-actions/configure-aws-credentials@f503a1870408dcf2c35d5c2b8a68e69211042c7d with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} From d31e6ad7c7fca31a5640f6cdc3f0870eef2c390c Mon Sep 17 00:00:00 2001 From: John Mumm Date: Mon, 7 Jul 2025 12:51:21 +0200 Subject: [PATCH 184/185] Move fragment preservation test to directly test our redirect handling logic (#14480) When [updating](https://github.com/astral-sh/uv/pull/14475) to the latest `reqwest` version, our fragment propagation test broke. That test was partially testing the `reqwest` behavior, so this PR moves the fragment test to directly test our logic for constructing redirect requests. --- crates/uv-client/src/base_client.rs | 39 +++++++++++++++++++++++++ crates/uv-client/src/registry_client.rs | 38 ------------------------ 2 files changed, 39 insertions(+), 38 deletions(-) diff --git a/crates/uv-client/src/base_client.rs b/crates/uv-client/src/base_client.rs index d62621863..e11845adb 100644 --- a/crates/uv-client/src/base_client.rs +++ b/crates/uv-client/src/base_client.rs @@ -982,6 +982,45 @@ mod tests { Ok(()) } + #[tokio::test] + async fn test_redirect_preserves_fragment() -> Result<()> { + for status in &[301, 302, 303, 307, 308] { + let server = MockServer::start().await; + Mock::given(method("GET")) + .respond_with( + ResponseTemplate::new(*status) + .insert_header("location", format!("{}/redirect", server.uri())), + ) + .mount(&server) + .await; + + let request = Client::new() + .get(format!("{}#fragment", server.uri())) + .build() + .unwrap(); + + let response = Client::builder() + .redirect(reqwest::redirect::Policy::none()) + .build() + .unwrap() + .execute(request.try_clone().unwrap()) + .await + .unwrap(); + + let redirect_request = + request_into_redirect(request, &response, CrossOriginCredentialsPolicy::Secure)? + .unwrap(); + assert!( + redirect_request + .url() + .fragment() + .is_some_and(|fragment| fragment == "fragment") + ); + } + + Ok(()) + } + #[tokio::test] async fn test_redirect_removes_authorization_header_on_cross_origin() -> Result<()> { for status in &[301, 302, 303, 307, 308] { diff --git a/crates/uv-client/src/registry_client.rs b/crates/uv-client/src/registry_client.rs index b53e1ed9a..5788ea56c 100644 --- a/crates/uv-client/src/registry_client.rs +++ b/crates/uv-client/src/registry_client.rs @@ -1416,44 +1416,6 @@ mod tests { Ok(()) } - #[tokio::test] - async fn test_redirect_preserve_fragment() -> Result<(), Error> { - let redirect_server = MockServer::start().await; - - // Configure the redirect server to respond with a 307 with a relative URL. - Mock::given(method("GET")) - .respond_with(ResponseTemplate::new(307).insert_header("Location", "/foo".to_string())) - .mount(&redirect_server) - .await; - - Mock::given(method("GET")) - .and(path_regex("/foo")) - .respond_with(ResponseTemplate::new(200)) - .mount(&redirect_server) - .await; - - let cache = Cache::temp()?; - let registry_client = RegistryClientBuilder::new(cache).build(); - let client = registry_client.cached_client().uncached(); - - let mut url = DisplaySafeUrl::parse(&redirect_server.uri())?; - url.set_fragment(Some("fragment")); - - assert_eq!( - client - .for_host(&url) - .get(Url::from(url.clone())) - .send() - .await? - .url() - .to_string(), - format!("{}/foo#fragment", redirect_server.uri()), - "Requests should preserve fragment" - ); - - Ok(()) - } - #[test] fn ignore_failing_files() { // 1.7.7 has an invalid requires-python field (double comma), 1.7.8 is valid From ddb1577a931bef1db26202095d84f0045688fb5c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 7 Jul 2025 13:39:57 +0200 Subject: [PATCH 185/185] Update Rust crate reqwest to v0.12.22 (#14475) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [reqwest](https://redirect.github.com/seanmonstar/reqwest) | workspace.dependencies | patch | `=0.12.15` -> `=0.12.22` | --- > [!WARNING] > Some dependencies could not be looked up. Check the Dependency Dashboard for more information. --- ### Release Notes
    seanmonstar/reqwest (reqwest) ### [`v0.12.22`](https://redirect.github.com/seanmonstar/reqwest/blob/HEAD/CHANGELOG.md#v01222) [Compare Source](https://redirect.github.com/seanmonstar/reqwest/compare/v0.12.21...v0.12.22) - Fix socks proxies when resolving IPv6 destinations. ### [`v0.12.21`](https://redirect.github.com/seanmonstar/reqwest/blob/HEAD/CHANGELOG.md#v01221) [Compare Source](https://redirect.github.com/seanmonstar/reqwest/compare/v0.12.20...v0.12.21) - Fix socks proxy to use `socks4a://` instead of `socks4h://`. - Fix `Error::is_timeout()` to check for hyper and IO timeouts too. - Fix request `Error` to again include URLs when possible. - Fix socks connect error to include more context. - (wasm) implement `Default` for `Body`. ### [`v0.12.20`](https://redirect.github.com/seanmonstar/reqwest/blob/HEAD/CHANGELOG.md#v01220) [Compare Source](https://redirect.github.com/seanmonstar/reqwest/compare/v0.12.19...v0.12.20) - Add `ClientBuilder::tcp_user_timeout(Duration)` option to set `TCP_USER_TIMEOUT`. - Fix proxy headers only using the first matched proxy. - (wasm) Fix re-adding `Error::is_status()`. ### [`v0.12.19`](https://redirect.github.com/seanmonstar/reqwest/blob/HEAD/CHANGELOG.md#v01219) [Compare Source](https://redirect.github.com/seanmonstar/reqwest/compare/v0.12.18...v0.12.19) - Fix redirect that changes the method to GET should remove payload headers. - Fix redirect to only check the next scheme if the policy action is to follow. - (wasm) Fix compilation error if `cookies` feature is enabled (by the way, it's a noop feature in wasm). ### [`v0.12.18`](https://redirect.github.com/seanmonstar/reqwest/blob/HEAD/CHANGELOG.md#v01218) [Compare Source](https://redirect.github.com/seanmonstar/reqwest/compare/v0.12.17...v0.12.18) - Fix compilation when `socks` enabled without TLS. ### [`v0.12.17`](https://redirect.github.com/seanmonstar/reqwest/blob/HEAD/CHANGELOG.md#v01217) [Compare Source](https://redirect.github.com/seanmonstar/reqwest/compare/v0.12.16...v0.12.17) - Fix compilation on macOS. ### [`v0.12.16`](https://redirect.github.com/seanmonstar/reqwest/blob/HEAD/CHANGELOG.md#v01216) [Compare Source](https://redirect.github.com/seanmonstar/reqwest/compare/v0.12.15...v0.12.16) - Add `ClientBuilder::http3_congestion_bbr()` to enable BBR congestion control. - Add `ClientBuilder::http3_send_grease()` to configure whether to send use QUIC grease. - Add `ClientBuilder::http3_max_field_section_size()` to configure the maximum response headers. - Add `ClientBuilder::tcp_keepalive_interval()` to configure TCP probe interval. - Add `ClientBuilder::tcp_keepalive_retries()` to configure TCP probe count. - Add `Proxy::headers()` to add extra headers that should be sent to a proxy. - Fix `redirect::Policy::limit()` which had an off-by-1 error, allowing 1 more redirect than specified. - Fix HTTP/3 to support streaming request bodies. - (wasm) Fix null bodies when calling `Response::bytes_stream()`.
    --- ### Configuration 📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/astral-sh/uv). --- Closes #14243 --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: konstin --- Cargo.lock | 91 ++++++++++++++++++++++++++++-------------------------- Cargo.toml | 2 +- 2 files changed, 48 insertions(+), 45 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ffadd8df2..ef7511af5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1698,7 +1698,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots", + "webpki-roots 0.26.8", ] [[package]] @@ -1707,6 +1707,7 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb" dependencies = [ + "base64 0.22.1", "bytes", "futures-channel", "futures-core", @@ -1714,7 +1715,9 @@ dependencies = [ "http", "http-body", "hyper", + "ipnet", "libc", + "percent-encoding", "pin-project-lite", "socket2", "tokio", @@ -1945,6 +1948,16 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "is-terminal" version = "0.4.15" @@ -3062,9 +3075,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.15" +version = "0.12.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" +checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" dependencies = [ "async-compression", "base64 0.22.1", @@ -3079,18 +3092,14 @@ dependencies = [ "hyper", "hyper-rustls", "hyper-util", - "ipnet", "js-sys", "log", - "mime", "mime_guess", - "once_cell", "percent-encoding", "pin-project-lite", "quinn", "rustls", "rustls-native-certs", - "rustls-pemfile", "rustls-pki-types", "serde", "serde_json", @@ -3098,17 +3107,16 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-rustls", - "tokio-socks", "tokio-util", "tower", + "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots", - "windows-registry 0.4.0", + "webpki-roots 1.0.1", ] [[package]] @@ -3351,15 +3359,6 @@ dependencies = [ "security-framework", ] -[[package]] -name = "rustls-pemfile" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "rustls-pki-types" version = "1.11.0" @@ -4172,18 +4171,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-socks" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f" -dependencies = [ - "either", - "futures-util", - "thiserror 1.0.69", - "tokio", -] - [[package]] name = "tokio-stream" version = "0.1.17" @@ -4266,6 +4253,24 @@ dependencies = [ "tower-service", ] +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags 2.9.1", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-layer" version = "0.3.3" @@ -5635,7 +5640,7 @@ dependencies = [ "uv-trampoline-builder", "uv-warnings", "which", - "windows-registry 0.5.3", + "windows-registry", "windows-result 0.3.4", "windows-sys 0.59.0", ] @@ -5839,7 +5844,7 @@ dependencies = [ "tracing", "uv-fs", "uv-static", - "windows-registry 0.5.3", + "windows-registry", "windows-result 0.3.4", "windows-sys 0.59.0", ] @@ -6221,6 +6226,15 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "webpki-roots" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8782dd5a41a24eed3a4f40b606249b3e236ca61adf1f25ea4d45c73de122b502" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "weezl" version = "0.1.8" @@ -6448,17 +6462,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-registry" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" -dependencies = [ - "windows-result 0.3.4", - "windows-strings 0.3.1", - "windows-targets 0.53.0", -] - [[package]] name = "windows-registry" version = "0.5.3" diff --git a/Cargo.toml b/Cargo.toml index 817c5c62b..fc19dcc9a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -142,7 +142,7 @@ ref-cast = { version = "1.0.24" } reflink-copy = { version = "0.1.19" } regex = { version = "1.10.6" } regex-automata = { version = "0.4.8", default-features = false, features = ["dfa-build", "dfa-search", "perf", "std", "syntax"] } -reqwest = { version = "=0.12.15", default-features = false, features = ["json", "gzip", "deflate", "zstd", "stream", "rustls-tls", "rustls-tls-native-roots", "socks", "multipart", "http2", "blocking"] } +reqwest = { version = "0.12.22", default-features = false, features = ["json", "gzip", "deflate", "zstd", "stream", "rustls-tls", "rustls-tls-native-roots", "socks", "multipart", "http2", "blocking"] } reqwest-middleware = { git = "https://github.com/astral-sh/reqwest-middleware", rev = "ad8b9d332d1773fde8b4cd008486de5973e0a3f8", features = ["multipart"] } reqwest-retry = { git = "https://github.com/astral-sh/reqwest-middleware", rev = "ad8b9d332d1773fde8b4cd008486de5973e0a3f8" } rkyv = { version = "0.8.8", features = ["bytecheck"] }